What is the correct way to select one of many child elements in React?

JoeMajor :

I have a small part of my new React app which contains a block of text, AllLines, split into line-by-line components called Line. I want to make it work so that when one line is clicked, it will be selected and editable and all other lines will appear as <p> elements. How can I best manage the state here such that only one of the lines is selected at any given time? The part I am struggling with is determining which Line element has been clicked in a way that the parent can change its state.

I know ways that I can make this work, but I'm relatively new to React and trying to get my head into 'thinking in React' by doing things properly so I'm keen to find out what is the best practice in this situation.

class AllLines extends Component {
    state = {
        selectedLine: 0,
        lines: []
    };

    handleClick = (e) => {
        console.log("click");
    };

    render() {
        return (
            <Container>
                {
                    this.state.lines.map((subtitle, index) => {
                        if (index === this.state.selectedLine) {
                            return (
                                <div id={"text-line-" + index}>
                                    <TranscriptionLine
                                        lineContent={subtitle.text}
                                        selected={true}
                                    />
                                </div>
                            )
                        }
                        return (
                            <div id={"text-line-" + index}>
                                <Line
                                    lineContent={subtitle.text}
                                    handleClick={this.handleClick}
                                />
                            </div>
                        )
                    })
                }
            </Container>
        );
    }
}
class Line extends Component {

    render() {
        if (this.props.selected === true) {
            return (
                <input type="text" value={this.props.lineContent} />
            )
        }
        return (
            <p id={} onClick={this.props.handleClick}>{this.props.lineContent}</p>
        );
    }
}
Yevgen Gorbunkov :

'Thinking in React' you would want to give up your habit to grab DOM elements by their unique id ;)

From what I see, there're few parts missing from your codebase:

  • smart click handler that will keep only one line selected at a time
  • edit line handler that will stick to the callback that will modify line contents within parent state
  • preferably two separate components for the line capable of editing and line being actually edited as those behave in a different way and appear as different DOM elements

To wrap up the above, I'd slightly rephrase your code into the following:

const { Component } = React,
      { render } = ReactDOM
      
const linesData = Array.from(
        {length:10},
        (_,i) => `There goes the line number ${i}`
      )      
      
class Line extends Component {
  render(){
    return (
      <p onClick={this.props.onSelect}>{this.props.lineContent}</p>
    )
  }
}

class TranscriptionLine extends Component {
  constructor(props){
    super(props)
    this.state = {
      content: this.props.lineContent
    }
    this.onEdit = this.onEdit.bind(this)
  }
  
  onEdit(value){
    this.setState({content:value})
    this.props.pushEditUp(value, this.props.lineIndex)
  }
  
  render(){
    return (
      <input
        style={{width:200}}
        value={this.state.content} 
        onChange={({target:{value}}) => this.onEdit(value)} 
      />
    )
  }
}

class AllLines extends Component {
    constructor (props) {
      super(props)
      this.state = {
        selectedLine: null,
        lines: this.props.lines
      }
      this.handleSelect = this.handleSelect.bind(this)
      this.handleEdit = this.handleEdit.bind(this)
    }
    
    handleSelect(idx){
      this.setState({selectedLine:idx})
    }
    
    handleEdit(newLineValue, lineIdx){
      const linesShallowCopy = [...this.state.lines]
      linesShallowCopy.splice(lineIdx,1,newLineValue)
      this.setState({
        lines: linesShallowCopy
      })
    }

    render() {
        return (
            <div>
                {
                    this.state.lines.map((text, index) => {
                        if(index === this.state.selectedLine) {
                            return (
                                    <TranscriptionLine
                                        lineContent={text}
                                        lineIndex={index}
                                        pushEditUp={this.handleEdit}
                                    />
                            )
                        }
                        else
                            return (
                                    <Line
                                        lineContent={text}
                                        lineIndex={index}
                                        onSelect={() => this.handleSelect(index)}
                                    />
                            )
                    })
                }
            </div>
        )
    }
}

render (
  <AllLines lines={linesData} />,
  document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=7216&siteId=1