Reactjs - Adding ref to input in dynamic element render

merrilj picture merrilj · Sep 28, 2017 · Viewed 18.2k times · Source

I'm trying to focus/highlight input text onClick in React. It works as expected, but only on the last element in the rendered array. I've tried several different methods but they all do the exact same thing. Here are two examples of what I have:

export default class Services extends Component {

handleFocus(event) {
    event.target.select()
}

handleClick() {
    this.textInput.focus()
}


render() {
    return (
        <div>
            {element.sources.map((el, i) => (
                <List.Item key={i}>
                <Segment style={{marginTop: '0.5em', marginBottom: '0.5em'}}>
                    <Input fluid type='text'
                        onFocus={this.handleFocus}
                        ref={(input) => { this.textInput = input }} 
                        value='text to copy'
                        action={
                            <Button inverted color='blue' icon='copy' onClick={() => this.handleClick}></Button>
                        }
                    />
                </Segment>
                </List.Item>
            ))}
        </div>
    )
}

If there's only one element being rendered, it focuses the text in the input, but if there are multiple elements, every element's button click selects only the last element's input. Here's another example:

export default class Services extends Component {

constructor(props) {
    super(props)

    this._nodes = new Map()
    this._handleClick = this.handleClick.bind(this)
}

handleFocus(event) {
    event.target.select()
}

handleClick(e, i) {
    const node = this._nodes.get(i)
    node.focus()
}


render() {
    return (
        <div>
            {element.sources.map((el, i) => (
                <List.Item key={i}>
                <Segment style={{marginTop: '0.5em', marginBottom: '0.5em'}}>
                    <Input fluid type='text'
                        onFocus={this.handleFocus}
                        ref={c => this._nodes.set(i, c)} 
                        value='text to copy'
                        action={
                            <Button inverted color='blue' icon='copy' onClick={e => this.handleClick(e, i)}></Button>
                        }
                    />
                </Segment>
                </List.Item>
            ))}
        </div>
    )
}

Both of these methods basically respond the same way. I need the handleClick input focus to work for every dynamically rendered element. Any advice is greatly appreciated. Thanks in advance!

The Input component is imported from Semantic UI React with no additional implementations in my app

UPDATE Thanks guys for the great answers. Both methods work great in a single loop element render, but now I'm trying to implement it with multiple parent elements. For example:

import React, { Component } from 'react'
import { Button, List, Card, Input, Segment } from 'semantic-ui-react'

export default class ServiceCard extends Component {

handleFocus(event) {
    event.target.select()
}

handleClick = (id) => (e) => {
    this[`textInput${id}`].focus()
}

render() {
    return (
        <List divided verticalAlign='middle'>
            {this.props.services.map((element, index) => (
                <Card fluid key={index}>
                    <Card.Content>
                        <div>
                            {element.sources.map((el, i) => (
                                <List.Item key={i}>
                                    <Segment>
                                        <Input fluid type='text'
                                            onFocus={this.handleFocus}
                                            ref={input => { this[`textInput${i}`] = input }} 
                                            value='text to copy'
                                            action={
                                                <Button onClick={this.handleClick(i)}></Button>
                                            }
                                        />
                                    </Segment>
                                </List.Item>
                            ))}
                        </div>
                    </Card.Content>
                </Card>
            ))}
        </List>
    )
}

Now, in the modified code, your methods work great for one Card element, but when there are multiple Card elements, it still only works for the last one. Both Input Buttons work for their inputs respectively, but only on the last Card element rendered.

Answer

Sagiv b.g picture Sagiv b.g · Sep 28, 2017

You are setting a ref inside a loop, as you already know, the ref is set to the class via the this key word. This means that you are setting multiple refs but overriding the same one inside the class.
One solution (not the ideal solution) is to name them differently, maybe add the key to each ref name:

        ref={input => {
          this[`textInput${i}`] = input;
        }}

and when you target that onClick event of the Button you should use the same key as a parameter:

 action={
                  <Button
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick(i)}
                  >
                    Focus
                  </Button>
                }

Now, the click event should change and accept the id as a parameter and trigger the relevant ref (i'm using currying here):

  handleClick = (id) => (e) => {
      this[`textInput${id}`].focus();
  }

Note that this is and easier solution but not the ideal solution, as we create a new instance of a function on each render, hence we pass a new prop which can interrupt the diffing algorithm of react (a better and more "react'ish" way coming next).

Pros:

  • Easier to implement
  • Faster to implement

Cons:

  • May cause performance issues
  • Less the react components way

Working example

This is the full Code:

class Services extends React.Component {

  handleFocus(event) {
    event.target.select();
  }


  handleClick = id => e => {
    this[`textInput${id}`].focus();
  };

  render() {
    return (
      <div>
        {sources.map((el, i) => (
          <List.Item key={i}>
            <Segment style={{ marginTop: "0.5em", marginBottom: "0.5em" }}>
              <Input
                fluid
                type="text"
                onFocus={this.handleFocus}
                ref={input => {
                  this[`textInput${i}`] = input;
                }}
                value="text to copy"
                action={
                  <Button
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick(i)}
                  >
                    Focus
                  </Button>
                }
              />
            </Segment>
          </List.Item>
        ))}
      </div>
    );
  }
}

render(<Services />, document.getElementById("root"));

A better and more "react'ish" solution would be to use component composition or a HOC that wraps the Button and inject some simple logic, like pass the id instead of using 2 functions in the parent.

Pros:

  • As mentioned, Less chances of performance issues
  • You can reuse this component and logic
  • Sometimes easier to debug

Cons:

  • More code writing

  • Another component to maintain / test etc..

A working example
The full Code:

class MyButton extends React.Component {

  handleClick = (e) =>  {
    this.props.onClick(this.props.id)
  }

  render() {
    return (
      <Button
      {...this.props}
        onClick={this.handleClick}
      >
        {this.props.children}
      </Button>
    )
  }
}


class Services extends React.Component {

  constructor(props){
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleFocus(event) {
    event.target.select();
  }


  handleClick(id){
    this[`textInput${id}`].focus();
  };

  render() {
    return (
      <div>
        {sources.map((el, i) => (
          <List.Item key={i}>
            <Segment style={{ marginTop: "0.5em", marginBottom: "0.5em" }}>
              <Input
                fluid
                type="text"
                onFocus={this.handleFocus}
                ref={input => {
                  this[`textInput${i}`] = input;
                }}
                value="text to copy"
                action={
                  <MyButton
                    inverted
                    color="blue"
                    icon="copy"
                    onClick={this.handleClick}
                    id={i}
                  >
                    Focus
                  </MyButton>
                }
              />
            </Segment>
          </List.Item>
        ))}
      </div>
    );
  }
}

render(<Services />, document.getElementById("root"));

Edit
As a followup to your edit:

but when there are multiple Card elements, it still only works for the last one.

This happens for the same reason as before, you are using the same i for both arrays.
This is an easy solution, use both index and i for your ref names.
Setting the ref name:

ref={input => { this[`textInput${index}${i}`] = input }}

Passing the name to the handler:

<Button onClick={this.handleClick(`${index}${i}`)}></Button>

Working example

I've modified my question and provided a second solution that is considered best practice. read my answer again and see the different approaches.