Are callbacks that are passed into other functional components updated on refresh?

Viewed 33

I have a complex form which contains input elements of different types. For simplicity I will limit this to one, since each effectively follows the same approach.

const Form = () => {
  const [state, setState] = useState();

  const handleChange = useCallback((name, value) => {
    setState({
      ...state,
      [name]: value
    })
  }, [state, setState]);

  useEffect(() => {
    console.log(state)
  }, [state, setState]);

  return (
    <div>
      <Input name="inputOne" onChange={handleChange} />
      <Input name="inputTwo" onChange={handleChange} />
    </div>
  );
}

const Input = ({ name, onChange }) => {
  const [value, setValue] = useState("");

  const handleChange = useCallback((e) => {
    setValue(e.target.value);
    onChange(name, e.target.value)
  }, [value, setValue]);

  return (
    <input onChange={handleChange} value={value}/>
  );
}

For some reason when using this form. If I enter values into the first input I get the result logged as

{ "inputOne": "value entered into input one" }

but then if I enter something into the second input I get this

{ "inputTwo": "value entered into input two" }

If I then go back and type into inputOne again we get this again.

{ "inputOne": "value entered into input one, and more text entered" }   

To my knowledge, the code I have written should update the callback when the state changes so the new state that is spread (...state) is the latest state value. This is then updated in all the elements that I pass the handleChange function into. However it appears that handleChange is not updated where it is passed into the onChange parameter so the old callback is called with the state property being saved incorrectly.

Can someone tell me if this is correct and that the handleChange callback isn't being updated correctly? It's like both Input are seeing different references to the handleChange callback.

1 Answers

The problem in your case is a very typical problem of closure.

It is quite complex as to how an old reference is obtained but let me try to explain it as best as I can

When you are creating your form component, your handleChange function is being updated on every state change, and being passed on to the Input component. However even though the updated instance is passed on to the child, the child is not using the updated instances unless its own value changed. Now when it tried to update its value, it is invoking the old referenced function which doesn't have the other input value reflected in it.

There are two ways to solve this problem

  • Use callback approach to setState, so that you are provided the latest state by react regardless of the value in its closure
  • Another way to solve it is to add onChange as a dependency to useCallback in Input component so that even if the other input elements caused the state the change, all Inputs have latest reference of the onChange function

const { useCallback, useState, useEffect } = React;
const Form = () => {
  const [state, setState] = useState({});

  const handleChange = useCallback((name, value) => {
    setState(prev=> ({
      ...prev,
      [name]: value
    }))
  }, [setState]);

  useEffect(() => {
    console.log(state);
  }, [state, setState]);

  return (
    <div>
      <Input name="inputOne" onChange={handleChange} />
      <Input name="inputTwo" onChange={handleChange} />
    </div>
  );
}

const Input = ({ name, onChange }) => {
  const [value, setValue] = useState("");

  const handleChange = useCallback((e) => {
    setValue(e.target.value);
    onChange(name, e.target.value)
  }, [value, setValue]);

  return (
    <input onChange={handleChange} value={value}/>
  );
}

ReactDOM.render(<Form />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app" />

P.S. If you are using useCallback and adding state as a dependency and you are updating state within itself, it is not achieving anything as a separate instance is created on each update. It is best to use functional approach to setState to get around this and for better memoization

Related