JoeMajor
JoeMajor

Reputation: 45

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

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>
        );
    }
}

Upvotes: 4

Views: 1344

Answers (2)

Yevhen Horbunkov
Yevhen Horbunkov

Reputation: 15550

'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>

Upvotes: 1

AbstractProblemFactory
AbstractProblemFactory

Reputation: 9811

In your case, there is no really simpler way. State of current selected Line is "above" line collection (parent), which is correct (for case where siblings need to know).

However, you could simplify your code a lot:

<Container>
{this.state.lines.map((subtitle, index) => (
    <div id={"text-line-" + index}>
        <Line
            handleClick={this.handleClick}
            lineContent={subtitle.text}
            selected={index === this.state.selectedLine}
        />
    </div>
))}
</Container>

and for Line component, it is good practice to use functional component, since it is stateless and even doesn't use any lifecycle method.

Edit: Added missing close bracket

Upvotes: 2

Related Questions