35

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>
    );
  }
}
BBaysinger
  • 6,614
  • 13
  • 63
  • 132
Alisa T Morgan
  • 667
  • 1
  • 7
  • 12
  • 1
    You need to references, these can not be dynamic. – Adeel Imran Sep 21 '18 at 16:57
  • Possible duplicate of [In react how to get ref of first element that's rendered from Map](https://stackoverflow.com/questions/47820031/in-react-how-to-get-ref-of-first-element-thats-rendered-from-map) – Sagiv b.g Sep 21 '18 at 18:06

6 Answers6

38

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>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • very useful solution – AL-zami Oct 12 '19 at 06:16
  • 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? –  Jan 21 '21 at 00:06
  • I am just wondering where (ref) is coming from as nothing is passed in... –  Jan 21 '21 at 00:07
  • The `this.setRef` function is passed to the `` component as ref. The `` component passes the ref to the ``, which calls the function and passes the `ref` to it via `(ref)`. Read about [callback refs](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs). – Ori Drori Jan 21 '21 at 00:10
  • 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? – JohnnyQ Sep 26 '21 at 20:01
  • I've added a 2nd part to the answer, that deals with changes such as changing/removing elements. – Ori Drori Sep 26 '21 at 20:30
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>
      );
 }
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Ahmed Magdy
  • 1,054
  • 11
  • 16
  • 1
    I don't see the cool part of having a .map to have a side effect like that... – nicoramirezdev May 29 '21 at 11:26
  • Although with some refactoring the pattern works and is readable, so I remove the down vote for the idea. – nicoramirezdev May 29 '21 at 11:29
  • 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. – JohnnyQ Sep 26 '21 at 19:17
2

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>
    )
}
Dylan L.
  • 1,243
  • 2
  • 16
  • 35
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

Ben Carp
  • 24,214
  • 9
  • 60
  • 72
0

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>
)
-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.

Kate
  • 120
  • 3
  • 13