kyun
kyun

Reputation: 10294

React useRef with Array for dynamic

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

Upvotes: 9

Views: 34673

Answers (5)

HMR
HMR

Reputation: 39340

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.

Upvotes: -2

Yaniv
Yaniv

Reputation: 21

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.

Upvotes: 1

Zohaib Ijaz
Zohaib Ijaz

Reputation: 22935

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

Upvotes: 18

wang
wang

Reputation: 612

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

Upvotes: 13

HermitCrab
HermitCrab

Reputation: 3274

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();

Upvotes: 0

Related Questions