BlogReactJS

ReactJS useRef array for adding multiple elements

Written by Codemzy on August 3rd, 2023

How can you `useRef` for multiple elements or inputs? What about adding new elements dynamically? Here's how you can create a `useRef` array or object for adding and removing multiple elements in your component.

Adding a ref to an element in ReactJS can be pretty easy.

import { useRef } from 'react';

function MyComponent() {
  // create the ref
  const inputRef = useRef(null);
  // add the ref
  return <input ref={inputRef} />;
};

And that works great if you have a static form or a single input. You can create a ref for the input, and focus it when the form displays.

// ref element
function SingleRef() {
  // the name input value
  const [name, setName] = React.useState("Codemzy");
  // the input ref
  const inputRef = React.useRef(null);
  
  // to focus the input
  function handleClick() {
    inputRef.current.focus();
  };
  
  return (
    <div className="py-10">
      <h2 className="text-lg font-bold">User</h2>
      <p className="text-gray-700 pb-4">A single input with a single ref.</p>
      <label htmlFor="name-input">Name</label>
      <input ref={inputRef} id="name-input" name="name" value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={handleClick}>Focus Me</button>
    </div>
  );
}

But what if you don't know how many elements you might have? Sticking with our form example, let's say you want users to create a team. And a team might have 2, 20, or even 50 members.

You could create 50 inputs and 50 refs. But that seems a little excessive - don't you think?

Since we need multiple inputs, but the number of inputs will be dynamic (depending on how many team members the user wants to add), we can be a little bit more imaginative with our refs.

Here's what we will build:

See the Pen ReactJS useRef by Codemzy (@codemzy) on CodePen.

Creating a useRef array

Your ref can be a value or a single DOM node, but it can also be an array of values or an array of elements.

Since we don't know how many inputs the user will need, using an array will be perfect. We can add elements as we go.

Here's the new code:

// dynamic ref array
function RefArray() {
  // the team input values
  const [team, setTeam] = React.useState([ "" ]);
  // the input refs array
  const inputRefs = React.useRef([]);

  // update a team member
  function updateMember(index, value) {
    setTeam([ ...team.slice(0, index), value, ...team.slice(index+1)]);
  };
  
  // to focus the input
  function handleClick(element) {
    inputRefs.current[element].focus();
  };
  
  return (
    <div className="py-10">
      <h2 className="text-lg font-bold">Team</h2>
      <p className="text-gray-700 pb-4">Dynamic inputs with an array ref.</p>
      {team.map((member, i) => {
        return (
          <div key={i}>
            <label htmlFor={`input-array-${i+1}`}>Member {i+1}</label>
            <input ref={(element) => inputRefs.current[i] = element} id={`input-array-${i+1}`} value={team[i]} onChange={(e) => updateMember(i, e.target.value)} />
            <button onClick={() => handleClick(i)}>Focus Me</button>
          </div>
        );
      })}
    </div>
  );
};

Let's see how it works.

First, our state will be an array (for the team values).

// instead of a single value state for name
// const [name, setName] = React.useState("Codemzy");
// we will create an array for a team
const [team, setTeam] = React.useState([ "" ]);

Now our ref will be initiated as an array (instead of null).

// const inputRef = React.useRef(null);
const inputRefs = React.useRef([]);

I've also renamed it inputRefs since it will store multiple values!

Instead of returning a single input, our component needs to map over our team values and return an input for each member.

Instead of assigning the ref like ref={inputRef}, we will pass a function ref={(element) => inputRefs.current[i] = element} that assigns the element to its position i in the inputRefs array.

{team.map((member, i) => {
  return (
    <div key={i}>
      <label htmlFor={`input-array-${i+1}`}>Member {i+1}</label>
      <input ref={(element) => inputRefs.current[i] = element} id={`input-array-${i+1}`} value={team[i]} onChange={(e) => updateMember(i, e.target.value)} />
      <button onClick={() => handleClick(i)}>Focus Me</button>
    </div>
  );
})}

I've added a function to handle the inputs onChange, because we now need to change the value for the item in the array and keep the other values.

// update a team member
function updateMember(index, value) {
  setTeam([ ...team.slice(0, index), value, ...team.slice(index+1)]);
};

You will also notice that our onClick function now takes the index i so that it knows which element to focus on.

// to focus the input
function handleClick(element) {
  inputRefs.current[element].focus();
};

That all works, but we can't yet add new members.

Dynamically adding multiple elements to useRef

We want users to be able to add as many team members as they like, and we can't create a new ref for each one, since we don't know ahead of time how many members they need.

The good news is we have already done all the hard work, setting up and wiring up our inputRefs array.

Let's add a function that adds new team members.

// add a team member
function addMember() {
  setTeam([ ...team, ""]);
};

That was simple!

All this function does is add a new item to the end of the team array in the state. And since our component already maps over the state, it will create a new element, and assign that in our inputRefs array (with our ref={(element) => inputRefs.current[i] = element} assignment function.

Let's add a "Add Member" button, and we are all wired up!

// dynamic ref array
function RefArray() {
  // the team input values
  const [team, setTeam] = React.useState([ "" ]);
  // the input refs array
  const inputRefs = React.useRef([]);
  
  // add a team member
  function addMember() {
    setTeam([ ...team, ""]);
  };
  
  // update a team member
  function updateMember(index, value) {
    setTeam([ ...team.slice(0, index), value, ...team.slice(index+1)]);
  };
  
  // to focus the input
  function handleClick(element) {
    inputRefs.current[element].focus();
  };
  
  return (
    <div className="py-10">
      <h2 className="text-lg font-bold">Team</h2>
      <p className="text-gray-700 pb-4">Dynamic inputs with an array ref.</p>
      {team.map((member, i) => {
        return (
          <div key={i}>
            <label htmlFor={`input-array-${i+1}`}>Member {i+1}</label>
            <input ref={(element) => inputRefs.current[i] = element} id={`input-array-${i+1}`} value={team[i]} onChange={(e) => updateMember(i, e.target.value)} />
            <button onClick={() => handleClick(i)}>Focus Me</button>
          </div>
        );
      })}
      <div className="my-5 -mx-1">
        <button onClick={addMember}>Add Member</button>
      </div>
    </div>
  );
};

Focus on the new element

I'd also love to be able to focus on the new element when it's added.

For example, the user clicks the "Add Member" button, so we know they want to add a new member to their team. It would be good to focus on the input, so they can start typing in the name right away.

We can't do this in addMember, because the element doesn't exist in inputRefs yet. It doesn't even exist in the state yet!

But we do know that we always add new team members to the end of the array.

So, we can useEffect to focus on the last item in the inputRefs array whenever the team state changes.

// focus on the last team member when the team state changes
React.useEffect(() => {
  inputRefs.current[team.length-1].focus();
}, [team]);

Here's the updated component:

// dynamic ref array
function RefArray() {
  // the team input values
  const [team, setTeam] = React.useState([ "" ]);
  // the input refs array
  const inputRefs = React.useRef([]);
  
  // focus on the last team member when the team state changes
  React.useEffect(() => {
    inputRefs.current[team.length-1].focus();
  }, [team]);
  
  // add a team member
  function addMember() {
    setTeam([ ...team, ""]);
  };
  
  // update a team member
  function updateMember(index, value) {
    setTeam([ ...team.slice(0, index), value, ...team.slice(index+1)]);
  };
  
  // to focus the input
  function handleClick(element) {
    inputRefs.current[element].focus();
  };
  
  return (
    <div className="py-10">
      <h2 className="text-lg font-bold">Team</h2>
      <p className="text-gray-700 pb-4">Dynamic inputs with an array ref.</p>
      {team.map((member, i) => {
        return (
          <div key={i}>
            <label htmlFor={`input-array-${i+1}`}>Member {i+1}</label>
            <input ref={(element) => inputRefs.current[i] = element} id={`input-array-${i+1}`} value={team[i]} onChange={(e) => updateMember(i, e.target.value)} />
            <button onClick={() => handleClick(i)}>Focus Me</button>
          </div>
        );
      })}
      <div className="my-5 -mx-1">
        <button onClick={addMember}>Add Member</button>
      </div>
    </div>
  );
};

Ok super, we are done! Our useRef array works for adding multiple elements, and we are all wired up.

You can leave it here, but I'm going to make a couple of changes for some extra features like:

  • removing elements
  • adding keys (so reordering will work)

Creating a useRef object

The useRef array works great for adding team members. But what if we needed to remove one? What if the user had 5 team members and needed to remove number 3?

So I can avoid adding any more buttons, let's make it so that when a user hits backspace on an empty input, it will get removed.

<input ref={(element) => inputRefs.current[i] = element} id={`input-array-${i+1}`} onKeyDown={(e) => { e.key === "Backspace" && !e.target.value && removeMember(i) }} />

Let's see that in action...

removing useref element with index

Wait, what?!

Well, we are using the array index (i) to get the right element in our inputRefs array, which is great when everything stays in the same order. It worked fine when we were just adding elements to the end of the array.

But we just removed an element in the middle of the array.

But as far as React is concerned, i number 3 is still there, we now have a team array length of 4. It thinks we just got rid of an element, like the last one. So our now empty input stayed and the last inputRef got removed instead - sorry Ethan!

Instead of using the position in the array, we should give each input a unique key prop, and that will allow us to remove and reorder to our heart's content.

Because if React gets a unique key for each element, it will know which one to remove when it re-renders!

Since each element will need a unique key, I prefer to use an object instead of an array for useRef when dealing with more complex situations like this.

Because two team members could have the same name, I'm going to start by creating a unique random ID for each team member.

// add a team member - now adds a unique id
function addMember() {
  let id = getId({ existing: team.map(member => member.id) }); // new unique id
  setTeam([ ...team, { id, value: "" }]);
};

See my blog post how to generate a random unique ID with JavaScript for the getId() code or there are dedicated libraries like uuid that can handle this for you.

And now I'm going to replace most instances of i with member.id. Take your time going through the changes, but in summary:

  • our teams array is now an array of objects { id: "123", value: "" }
  • inputRefs is now an object with each key being the unique id for the team member.
// dynamic ref object
function RefObject() {
  // the team input values
  const [team, setTeam] = React.useState([ { id: getId({}), value: "" } ]);
  // the input refs object
  const inputRefs = React.useRef({});
  
  // add a team member - now adds a unique id
  function addMember() {
    let id = getId({ existing: team.map(member => member.id) }); // new unique id
    setTeam([ ...team, { id, value: "" }]);
  };
  
  // update a team member - now takes unique id
  function updateMember(id, value) {
    setTeam([ ...team.map((member) => {
      if (member.id === id) {
        return {...member, value};
      }
      return member;
    })]);
  };
  
  // remove a team member - now takes unique id
  function removeMember(id) {
    setTeam(team.filter((member) => member.id !== id));
  };
  
  // to focus the input - now takes unique id
  function handleClick(id) {
    inputRefs.current[id].focus();
  };
  
  return (
    <div className="py-10">
      <h2 className="text-lg font-bold">Team</h2>
      <p className="text-gray-700 pb-4">Dynamic inputs with an object ref.</p>
      {team.map((member, i) => {
        return (
          <div key={member.id}>
            <label htmlFor={`input-array-${member.id}`}>Member {i+1}</label>
            <input ref={(element) => inputRefs.current[member.id] = element} id={`input-array-${member.id}`} value={member.value} onChange={(e) => updateMember(member.id, e.target.value)} onKeyDown={(e) => { e.key === "Backspace" && !e.target.value && removeMember(member.id) }} />
            <button onClick={() => handleClick(member.id)}>Focus Me</button>
          </div>
        );
      })}
      <div className="my-5 -mx-1">
        <button onClick={addMember}>Add Member</button>
      </div>
    </div>
  );
};

And now we can remove Clive, without any problems!

removing useref element with id

Bye Clive!

Focus on the new element (again!)

You might notice that the new input isn't focused when we add a new team member.

That's because we can't focus the last item in the inputRefs array anymore - because it's not an array!

And this wont work:

// add a team member - now adds a unique id
function addMember() {
  let id = getId({ existing: team.map(member => member.id) }); // new unique id
  setTeam([ ...team, { id, value: "" }]);
  inputRefs.current[id].focus(); // 🚨 wont work
};

Because inputRefs.current[id] doesn't exist until the team state is updated and the component has re-rendered!

So we need a little extra state.

I'll call it focusId.

// dynamic ref object
function RefObject() {
  // the team input values
  const [team, setTeam] = React.useState([ { id: getId({}), value: "" } ]);
// 🆕 for focus on an Id
  const [focusId, setFocusId] = React.useState(false);
  // the input refs object
  const inputRefs = React.useRef({});
  
 // 🆕 focus on a team member
  React.useEffect(() => {
    if (focusId) {
      inputRefs.current[focusId].focus();
      setFocusId(false);
    }
  }, [focusId]);
  
  // add a team member - now adds a unique id
  function addMember() {
    let id = getId({ existing: team.map(member => member.id) }); // new unique id
    setTeam([ ...team, { id, value: "" }]);
    setFocusId(id); // 🆕 to trigger focusing the new member once added to state
  };

  //...
};

Phew! That was a little longer than expected, but now we have two ways to useRef for multiple elements. We can use an array and access each element by its index, or for more complex use cases, create an object with unique keys to access each element.