36

Below is my parent component with multiple inputs from a loop. How can I choose one input to focus? Do I have to create a dynamic ref in this case?

class TestRef extends React.Component {
  ref = React.createRef();
  state = {
    data: [
      {
        name: "abc"
      },
      { name: "def" }
    ]
  };
  focusInput = () => this.ref.current.focus();
  render() {
    return (
      <div>
        {this.state.data.map(o => {
          return <Hello placeholder={o.name} ref={this.ref} />;
        })}
        <button onClick={this.focusInput}>focus input 1</button>
        <button onClick={this.focusInput}>focus input 2</button>
      </div>
    );
  }
}
2

6 Answers 6

40

You can use callback refs to generate and store the dynamic ref of each input in an array. Now you can refer to them using the index of the ref:

const Hello = React.forwardRef((props,  ref) => <input ref={ref} />);

class Button extends React.Component {
  onClick = () => this.props.onClick(this.props.id);

  render() {
    return (
      <button onClick={this.onClick}>{this.props.children}</button>
    );
  }
}

class TestRef extends React.Component {
  state = {
    data: [
      {
        name: "abc"
      },
      { name: "def" }
    ]
  };
  
  inputRefs = [];
  
  setRef = (ref) => {
    this.inputRefs.push(ref);
  };
  
  focusInput = (id) => this.inputRefs[id].focus();
  
  render() {
    return (
      <div>
        {this.state.data.map(({ name }) => (
          <Hello 
            placeholder={name} 
            ref={this.setRef} 
            key={name} />
        ))}
        <Button onClick={this.focusInput} id={0}>focus input 1</Button>
        <Button onClick={this.focusInput} id={1}>focus input 2</Button>
      </div>
    );
  }
}

ReactDOM.render(<TestRef />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

If the list wouldn't be static, and items may be removed/replaced, you should probably use a WeakMap to hold the refs, or any other method of adding the ref by a constant id. You should also check before using the ref, because it might not exist:

const Hello = React.forwardRef((props,  ref) => <input ref={ref} />);

class Button extends React.Component {
  onClick = () => this.props.onClick(this.props.id);

  render() {
    return (
      <button onClick={this.onClick}>{this.props.children}</button>
    );
  }
}

class TestRef extends React.Component {
  state = {
    data: [{ name: "abc" }, { name: "def" }, { name: "ghi" }]
  };
  
  componentDidMount() {
    setTimeout(() => {
      this.setState(({ data }) => ({
        data: data.slice(0, -1)
      }))
    }, 3000);
  }
  
  inputRefs = new WeakMap;
  
  setRef = (id) => (ref) => {
    this.inputRefs.set(id, ref);
  };
  
  focusInput = (id) => {
    const input = this.inputRefs.get(id);
    
    if(input) input.focus(); // use only if the ref exists - use optional chaining ?. if possible instead
  }
  
  render() {
    const { data } = this.state;
  
    return (
      <div>
        {data.map(o => (
          <Hello 
            placeholder={o.name} 
            ref={this.setRef(o)} 
            key={o.name} />
        ))}
        
        <br />
        
        {data.map((o, i) => (
          <Button onClick={this.focusInput} id={o} key={o.name}>focus input {i + 1}</Button>
        ))}
      </div>
    );
  }
}

ReactDOM.render(<TestRef />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Sign up to request clarification or add additional context in comments.

6 Comments

very useful solution
does this not work with a functional componenet... i see you have a function generating the refereces as they are loaded in. i see the ref argument but i do not see it passed in as a parameter? Is this a special key word or am i missing something?
I am just wondering where (ref) is coming from as nothing is passed in...
The this.setRef function is passed to the <Hello> component as ref. The <Hello> component passes the ref to the <input>, which calls the function and passes the ref to it via (ref). Read about callback refs.
What happens if the first Hello component gets removed? Are the refs still consistent? e.g. refs[0] still points to the old Hello that gets removed isn't it?
|
18

if you are coming to this question 2020 here is how you create multiple refs using create hooks in a loop

   const MyComponent=(){
    // empty list to put our refs in
    let LiRefs = []
    
    return (
        <React.Fragment>
          <ul className="event-list">
            // Check if my data exists first otherwise load spinner 
            {newData ? (
              newData.map((D) => {
                // the cool part inside the loop 
                // create new ref 
                // push it into array 

                const newRef = createRef();
                LiRefs.push(newRef);
                return (
                  // link it to your li 
                  // now you have list of refs that points to your list items 
                  <li key={D._id} ref={newRef}>
                    title : {D.title} <br />
                    description : {D.description}
                    <br />
                    data : {D.date} <br />
                    price : {D.price}
                    <br />
                    <div>creator : {D.creator.username}</div>
                    {authData.token && (
                      <button type="button" id={D._id} onClick={handelBooking}>
                        Book
                      </button>
                    )}
                  </li>
                );
              })
            ) : (
              <Spinner />
            )}
          </ul>
        </React.Fragment>
      );
 }

3 Comments

I don't see the cool part of having a .map to have a side effect like that...
Although with some refactoring the pattern works and is readable, so I remove the down vote for the idea.
I would expect the 2020 answer to be using hooks rather than createRef. There are caveats in this approach especially when the list is dynamic. useRef should be used rather than createRef or even better callback ref.
3

The correct way of adding refs to an array using hooks is by creating a useRef([]) then filling the array with your refs, then assigning the refs to the element. Like so:

import React, {useRef} from 'react'

const myComponent = ({items}) => {

    const refArr = useRef([])
    refArr.current = items.map((item, index) => {
        return refArr.current[index] || React.createRef();
    }

    return (
        <div>
            {items.map((item, index) => (
                <button ref={refArr.current[index]}>click me</button>
            )}
        </div>
    )
}

Comments

1

This is the way I did it for React Functional Programming and Typescript!

First create the ref array

   const refsById = useMemo(() => {
    const refs: any = {};
    items.forEach((item: ItemType, index: number) => {
        refs[index] = React.createRef(); 
    })
    return refs;
}, [items])

Then add the ref in your template

return (
    <div>
        {items.map((item: ItemType, index: number) => {
            return <input key={item.id} ref={refsById[index]} />
        })}
    </div>
)

Comments

0

Using a general useFocus hook

// General Focus Hook
const useFocus = (initialFocus = false, id = "") => {
    const [focus, setFocus] = useState(initialFocus)
    return ([
        (newVal=true) => setFocus(newVal), {
            autoFocus: focus,
            key: `${id}${focus}`,
            onFocus: () => setFocus(true),
            onBlur: () => setFocus(false),
        },
    ])
}

const data: [{
        name: "abc"
    },{ 
        name: "def" 
}]

const TestRef = () => {

    const focusHelper = data.map( (_,i) => {
        const [setFocus, focusProps]= useFocus(false, i)
        return {setFocus, focusProps}
    }) 

    return (
      <div>
        {data.map( (o,i) => (
          <Hello placeholder={o.name} {...focusHelper[i].focusProps} />;
        ))}
        <button onClick={() => focusHelper[0].setFocus()}>focus input 1</button>
        <button onClick={() => focusHelper[1].setFocus()}>focus input 2</button>
      </div>
    );
}

You can find more info here: Set focus on input after render

Comments

-1

I discovered another way of tackling this:

let dataCount = 0;

class TestRef extends React.Component {
  state = {
    data: [
      {
        name: "abc"
      },
      { name: "def" }
    ]
  };
  focusInput = (thisHello) => this[`ref${thisHello}`].current.focus();
  render() {
    return (
      <div>
        {this.state.data.map(o => {
          dataCount++
          return <Hello placeholder={o.name} ref={(el) => { this[`ref${dataCount}`] = el; }} />;
        })}
        <button onClick={() => this.focusInput(1)}>focus input 1</button>
        <button onClick={() => this.focusInput(2)}>focus input 2</button>
      </div>
    );
  }
}

The dataCount is unnecessary if your Hello element has a key or unique ID to use as a variable.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.