最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Reactjs - Adding ref to input in dynamic element render - Stack Overflow

programmeradmin2浏览0评论

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.

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.

Share Improve this question edited Sep 28, 2017 at 20:20 merrilj asked Sep 28, 2017 at 16:52 merriljmerrilj 3671 gold badge2 silver badges11 bronze badges 10
  • Possible duplicate of React Select mapping issue – bennygenel Commented Sep 28, 2017 at 16:55
  • It's different in the fact that other methods on the input work fine for every element besides handleClick. The ref only selects the last element rendered and no others. – merrilj Commented Sep 28, 2017 at 17:02
  • @MerrilJeffs The second code will work as expected. Are you getting any error on console for second code? – Prakash Sharma Commented Sep 28, 2017 at 17:27
  • show the code for your Input component to see how you implement the action (Button) events – Sagiv b.g Commented Sep 28, 2017 at 18:45
  • 1 @MerrilJeffs Second code is working. Here is the working example codesandbox.io/s/p3y90wmp7m – Prakash Sharma Commented Sep 28, 2017 at 19:12
 |  Show 5 more comments

2 Answers 2

Reset to default 12

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.

Complete Running example:

import React,{useRef} from 'react';

function Userlist(){
    let toogleRef = useRef([])
        
    const userlist = [
        {name:'ankit',email:'[email protected]',phone:222},
        {name:'verma',email:'[email protected]',phone:33},
        {name:'sumit',email:'[email protected]',phone:444},
    ];
    function toogleFun(ind){
        
        console.log(toogleRef.current);
        toogleRef.current[ind].style.display="none";        
    }
    return(
    <>
        <table>
        <tbody>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Email</th>
                <th>Phone</th>
                <th>Toggle</th>
            </tr>
            {
                userlist.map((item, index)=>
                        <tr key={index}  ref={(element) => toogleRef.current.push(element)}>
                            <td>{index + 1}</td>
                            <td>{item.name}</td>
                            <td>{item.email}</td>
                            <td>{item.phone}</td>
                            <td><button onClick={()=>toogleFun(index)}>Toogle</button></td>
                        </tr>
                )   
            }
        </tbody>
        </table>
    </>
    )
}
export default Userlist;
发布评论

评论列表(0)

  1. 暂无评论