Matt
Matt

Reputation: 99

React state variable not accurate after set state in useState hook

I have a React function component running along side a legacy JQuery application. When calling an event handler on a JQuery element, passing in the current React state defaults to the initial state value and not the updated state value.

Verified x state is changing through a useEffect hook, but when calling the event listener, x is set to the initial state value and not the state value after the update.

function MyComponent(props) {
   const [x, setX] = useState(false);

// On initial render
useEffect(() => {
   props.jQueryButton.addEventListener('click', onXSave)
}, [])

useEffect(() => {
    console.log("x changed " + x); // everytime onXChange is called, x 
    state is updated with the checked value, and this console.log shows 
    the correct state value
}, [x]);

onXChange = event => {
   setX(event.target.checked); // checked is true in this context
};

onXSave = event => {
  const obj = { x: x}; // x is false here, even though the state in this context shows it to be true.
  };
}

No error messages are displayed. In the above code in this context I expect the x state to be true in the onXSave method call, but it keeps displaying as false.

Upvotes: 3

Views: 1265

Answers (2)

Will Jenkins
Will Jenkins

Reputation: 9787

The version of onXSave that you're adding to your eventListener is out of date - you only add it once on the first render, so when x updates and causes a re-render, your useEffect doesn't run again and your jQueryButton will still holding on to the original function, which has closed over an out of date version of x.

You need to do two things:

  1. Add onXSave and your jQueryButton as dependencies in your useEffect dependency array (so when it re-renders, the current version of your function is hooked up to your eventListener)
  2. Remove the old event listener on re-renders by returning a clean-up function from your useEffect.

So something like:

function MyComponent(props) {
   const [x, setX] = useState(false);

// On initial render
useEffect(() => {
   props.jQueryButton.addEventListener('click', onXSave)
   return ()=> props.jQueryButton.removeEventListener('click', onXSave)
}, [onXSave, props.jQueryButton])

useEffect(() => {
    console.log("x changed " + x); // everytime onXChange is called, x 
    state is updated with the checked value, and this console.log shows 
    the correct state value
}, [x]);

onXChange = event => {
   setX(event.target.checked); // checked is true in this context
};

onXSave = event => {
  const obj = { x: x}; // x is false here, even though the state in this context shows it to be true.
  };


}

Upvotes: 1

marzelin
marzelin

Reputation: 11600

onXSave is added as a handler on initial render so x has the value from that time. It doesn't matter that onXSave is recreated on each render since it's never used past initial render.

To fix this you might put x into a ref

unction MyComponent(props) {
   const [x, setX] = useState(false);
   const ref = useRef();
   ref.current = x;

// On initial render
useEffect(() => {
   props.jQueryButton.addEventListener('click', onXSave)
}, [])

useEffect(() => {
    console.log("x changed " + x); // everytime onXChange is called, x 
    state is updated with the checked value, and this console.log shows 
    the correct state value
}, [x]);

onXChange = event => {
   setX(event.target.checked); // checked is true in this context
};

onXSave = event => {
  const obj = { x: ref.current}; // by using ref, the value is always current
  };
}

Upvotes: 1

Related Questions