7

I am trying to make the component that the focus moves to the next input when each letter inputted.

I think I need multiple ref like an array but I don't know about it.

It's a sample code for the question.

function PIN({length, onChange, value}){
  const inputEl = React.useRef(null);
  function handleChange(e){
    onChange(e);
    inputEl.current.focus(); 
  }
  return (
    <div>
      {
        new Array(length).fill(0).map((i)=>(
          <input type="text" ref={inputEl} onChange={handleChange} />
        ))
      }
    </div>
  )
}
kyun
  • 9,710
  • 9
  • 31
  • 66

5 Answers5

17

You can create multiple refs

function PIN({length, onChange, value}){
  const inputRefs = useMemo(() => Array(length).fill(0).map(i=> React.createRef()), []);
  const handleChange = index => (e) => {
    //onChange(e); // don't know about the logic of this onChange if you have multiple inputs
    if (inputRefs[index + 1]) inputRefs[index + 1].current.focus(); 
  }
  return (
    <div>
      {
        new Array(length).fill(0).map((inp, index)=>(
          <input type="text" ref={inputRefs[index]} onChange={handleChange(index)} />
        ))
      }
    </div>
  )
}
Zohaib Ijaz
  • 21,926
  • 7
  • 38
  • 60
  • 1
    Im just getting an error `React Hook "React.useRef" cannot be called inside a callback` not sure how this is the accepted anser. – HMR Apr 16 '20 at 08:56
  • 2
    If you are looking for the answer in typescript: `const inputRefs: React.RefObject[] = useMemo( ...` – Sanchitos May 28 '21 at 20:40
  • Another way of generating an array with less HOF `const inputRefs = useMemo(Array.from({length: 3}, x => React.createRef()), [])` https://2ality.com/2018/12/creating-arrays.html#creating-ranges-of-integer-values – Michael Guild Mar 22 '22 at 15:20
10

The ref on input is equivalent to a callback function. You can pass a method to him. The parameter received by this method is the input dom element, which you can store in an array.

import React from "react";
import "./styles.css";

export default function App() {
  const inputEl = React.useRef([]);
  function handleChange(i){
    inputEl.current[i+1].focus(); 
  }
  return (
    <div>
      {
        new Array(3).fill(0).map((n,i)=>(
          <input 
          key={i} 
          type="text" 
          ref={ref=>inputEl.current.push(ref)} 
          onChange={()=>handleChange(i)} 
          />
        ))
      }
    </div>
  )
}
wang
  • 562
  • 6
  • 19
  • perfect! hat's by far the best solution to this I've seen so far. – wenzf Nov 18 '20 at 17:33
  • 6
    Doesn't this cause an ever-expanding array? Let's say you have 3 input elements. After first render the array length will be 3. But after a re-render, those 3 items will be pushed again. So yes, the first 3 array elements are intact, but the array length is now 6 (and then 9, 12 and so on...) – targumon Feb 25 '21 at 15:55
  • @targumon a cleanup function in useEffect can empty the ref array. – Ahashan Alam Sojib Jul 28 '21 at 04:50
  • 4
    @AhashanAlamSojib sure, but why solve a problem if you can just avoid it? This is what I do: `ref={ref => { if (ref) elements.current[key] = ref }}` (elements is not an array but a simple JS object and key is the same one used anyway in the map function) – targumon Jul 28 '21 at 15:33
0

In your inputs, you can pass a function to the ref parameter, this will allow you to store all of your refs in an array:

let myRefs = [];

const saveThisRef = (element) => {
  myRefs.push(element);
}

Then you can pass your function to each input you render:

<input type="text" ref={saveThisRef} onChange={handleChange} />

Then you can advance to the next input in the onChange handler:

// Find the index of the next element
const index = myRefs.indexOf(element) + 1;
// Focus it
if (index < myRefs.length) myRefs[index].focus();
HermitCrab
  • 3,194
  • 1
  • 11
  • 10
0

Re-rendering the component that holds the dynamic Refs list with a different number of refs raises an exception ("Rendered more hooks than during the previous render"), as you can see in this example:

https://codesandbox.io/s/intelligent-shannon-u3yo6?file=/src/App.js

You can create a new component that renders a single and holds it's own single ref, and use the parent element to manage the current focused input, and pass this data to you'r new component, for example.

Yaniv
  • 11
  • 1
-2

Here is an example that would actually work:

const { useState, useCallback, useEffect, useRef } = React;

const Pin = ({ length, onChange, value }) => {
  const [val, setVal] = useState(value.split(''));
  const [index, setIndex] = useState(0);
  const arr = [...new Array(length)].map(
    (_, index) => index
  );
  const myRefs = useRef(arr);
  const saveThisRef = (index) => (element) => {
    myRefs.current[index] = element;
  };
  function handleChange(e) {
    const newVal = [...val];
    newVal[index] = e.target.value;
    if (index < length - 1) {
      setIndex(index + 1);
    }
    setVal(newVal);
    onChange(newVal.join(''));
  }
  const onFocus = (index) => () => {
    const newVal = [...val];
    newVal[index] = '';
    setIndex(index);
    setVal(newVal);
    onChange(newVal.join(''));
  };
  useEffect(() => {
    if (index < myRefs.current.length) {
      myRefs.current[index].focus();
    }
  }, [index, length, myRefs]);
  return arr.map((index) => (
    <input
      type="text"
      ref={saveThisRef(index)}
      onChange={handleChange}
      onFocus={onFocus(index)}
      value={val[index] || ''}
      maxLength="1"
      key={index}
    />
  ));
};
const App = () => {
  const [value, setValue] = useState('');
  const onChange = useCallback(
    (value) => setValue(value),
    []
  );
  console.log('value:', value);
  return (
    <Pin
      length={5}
      value={value}
      onChange={onChange}
    />
  );
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


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

All answers will shift focus to next input when you correct an already set value. The requirement is that focus should shift when a letter is inputted, not when you remove a value.

E_net4
  • 27,810
  • 13
  • 101
  • 139
HMR
  • 37,593
  • 24
  • 91
  • 160