Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React hooks: accessing up-to-date state from within a callback

EDIT (22 June 2020): as this question has some renewed interest, I realise there may be a few points of confusion. So I would like to highlight: the example in the question is intended as a toy example. It is not reflective of the problem. The problem which spurred this question, is in the use a third party library (over which there is limited control) that takes a callback as argument to a function. What is the correct way to provide that callback with the latest state. In react classes, this would be done through the use of this. In React hooks, due to the way state is encapsulated in the functions of React.useState(), if a callback gets the state through React.useState(), it will be stale (the value when the callback was setup). But if it sets the state, it will have access to the latest state through the passed argument. This means we can potentially get the latest state in such a callback with React hooks by setting the state to be the same as it was. This works, but is counter-intuitive.

-- Original question continues below --

I am using React hooks and trying to read state from within a callback. Every time the callback accesses it, it's back at its default value.

With the following code. The console will keep printing Count is: 0 no matter how many times I click.

function Card(title) {
  const [count, setCount] = React.useState(0)
  const [callbackSetup, setCallbackSetup] = React.useState(false)
  
  function setupConsoleCallback(callback) {
    console.log("Setting up callback")
    setInterval(callback, 3000)
  }

  function clickHandler() {
    setCount(count+1);
    if (!callbackSetup) {
      setupConsoleCallback(() => {console.log(`Count is: ${count}`)})
      setCallbackSetup(true)
    }
  }
  
  
  return (<div>
      Active count {count} <br/>
      <button onClick={clickHandler}>Increment</button>
    </div>);
  
}

const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component' />, el);

You can find this code here

I've had no problem setting state within a callback, only in accessing the latest state.

If I was to take a guess, I'd think that any change of state creates a new instance of the Card function. And that the callback is referring to the old one. Based on the documentation at https://reactjs.org/docs/hooks-reference.html#functional-updates, I had an idea to take the approach of calling setState in the callback, and passing a function to setState, to see if I could access the current state from within setState. Replacing

setupConsoleCallback(() => {console.log(`Count is: ${count}`)})

with

setupConsoleCallback(() => {setCount(prevCount => {console.log(`Count is: ${prevCount}`); return prevCount})})

You can find this code here

That approach hasn't worked either. EDIT: Actually that second approach does work. I just had a typo in my callback. This is the correct approach. I need to call setState to access the previous state. Even though I have no intention of setting the state.

I feel like I've taken similar approaches with React classes, but. For code consistency, I need to stick with React Effects.

How can I access the latest state information from within a callback?

like image 776
rod Avatar asked Oct 12 '22 10:10

rod


People also ask

How do you use callbacks in React Hooks?

The useCallback hook is used when you have a component in which the child is rerendering again and again without need. Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed.

How do you update state immediately in React?

To update state in React components, we'll use either the this. setState function or the updater function returned by the React. useState() Hook in class and function components, respectively.

Do Hooks share state between components?

Sharing states We can see that Hooks states works exactly like class component states. Every instance of the component has its own state. To work a solution which shares state between components, we will create a custom Hook. The idea is to create an array of listeners and only one state object.

Can we pass callback function in useState?

The `setState` above would throw warning and don't call `myCallback` because `useState` does not support callbacks and say that you should use `useEffect` for this purpose. So lets try to fix this in place using `useEffect` hook.


3 Answers

For your scenario (where you cannot keep creating new callbacks and passing them to your 3rd party library), you can use useRef to keep a mutable object with the current state. Like so:

function Card(title) {
  const [count, setCount] = React.useState(0)
  const [callbackSetup, setCallbackSetup] = React.useState(false)
  const stateRef = useRef();

  // make stateRef always have the current count
  // your "fixed" callbacks can refer to this object whenever
  // they need the current value.  Note: the callbacks will not
  // be reactive - they will not re-run the instant state changes,
  // but they *will* see the current value whenever they do run
  stateRef.current = count;

  function setupConsoleCallback(callback) {
    console.log("Setting up callback")
    setInterval(callback, 3000)
  }

  function clickHandler() {
    setCount(count+1);
    if (!callbackSetup) {
      setupConsoleCallback(() => {console.log(`Count is: ${stateRef.current}`)})
      setCallbackSetup(true)
    }
  }


  return (<div>
      Active count {count} <br/>
      <button onClick={clickHandler}>Increment</button>
    </div>);

}

Your callback can refer to the mutable object to "read" the current state. It will capture the mutable object in its closure, and every render the mutable object will be updated with the current state value.

like image 206
Brandon Avatar answered Oct 18 '22 05:10

Brandon


Update June 2021:

Use the NPM module react-usestateref to get always the latest state value. It's fully backward compatible with React useState API.

Example code how to use it:

import useState from 'react-usestateref';

const [count, setCount, counterRef] = useState(0);

console.log(couterRef.current); // it will always have the latest state value
setCount(20);
console.log(counterRef.current);

The NPM package react-useStateRef lets you access the latest state (like ref), by using useState.

Update Dec 2020:

To solve exactly this issue I have created a react module for that. react-usestateref (React useStateRef). E.g. of use:

var [state, setState, ref] = useState(0);

It's works exaclty like useState but in addition, it gives you the current state under ref.current

Learn more:

  • https://www.npmjs.com/package/react-usestateref

Original Answer

You can get the latest value by using the setState

For example:

var [state, setState] = useState(defaultValue);

useEffect(() => {
   var updatedState;
   setState(currentState => { // Do not change the state by getting the updated state
      updateState = currentState;
      return currentState;
   })
   alert(updateState); // the current state.
})
like image 59
Aminadav Glickshtein Avatar answered Oct 18 '22 04:10

Aminadav Glickshtein


I encountered a similar bug trying to do exactly the same thing you're doing in your example - using a setInterval on a callback that references props or state from a React component.

Hopefully I can add to the good answers already here by coming at the problem from a slightly different direction - the realisation that it's not even a React problem, but a plain old Javascript problem.

I think what catches one out here is thinking in terms of the React hooks model, where the state variable, just a local variable after all, can be treated as though it's stateful within the context of the React component. You can be sure that at runtime, the value of the variable will always be whatever React is holding under the hood for that particular piece of state.

However, as soon as you break out of the React component context - using the variable in a function inside a setInterval for instance, the abstraction breaks and you're back to the truth that that state variable really is just a local variable holding a value.

The abstraction allows you to write code as if the value at runtime will always reflect what's in state. In the context of React, this is the case, because what happens is whenever you set the state the entire function runs again and the value of the variable is set by React to whatever the updated state value is. Inside the callback, however, no such thing happens - that variable doesn't magically update to reflect the underlying React state value at call time. It just is what it is when the callback was defined (in this case 0), and never changes.

Here's where we get to the solution: if the value pointed to by that local variable is in fact a reference to a mutable object, then things change. The value (which is the reference) remains constant on the stack, but the mutable value(s) referenced by it on the heap can be changed.

This is why the technique in the accepted answer works - a React ref provides exactly such a reference to a mutable object. But I think it's really important to emphasise that the 'React' part of this is just a coincidence. The solution, like the problem, has nothing to do with React per-se, it's just that a React ref happens to be one way to get a reference to a mutable object.

You can also use, for instance, a plain Javascript class, holding its reference in React state. To be clear, I'm not suggesting this is a better solution or even advisable (it probably isn't!) but just using it to illustrate the point that there is no 'React' aspect to this solution - it's just Javascript:

class Count {
  constructor (val) { this.val = val }
  get () { return this.val }
  
  update (val) {
    this.val += val
    return this
  }
}

function Card(title) {
  const [count, setCount] = React.useState(new Count(0))
  const [callbackSetup, setCallbackSetup] = React.useState(false)
  
  function setupConsoleCallback(callback) {
    console.log("Setting up callback")
    setInterval(callback, 3000)
  }

  function clickHandler() {
    setCount(count.update(1));
    if (!callbackSetup) {
      setupConsoleCallback(() => {console.log(`Count is: ${count.get()}`)})
      setCallbackSetup(true)
    }
  }
  
  
  return (
    <div>
      Active count {count.get()} <br/>
      <button onClick={clickHandler}>Increment</button>
    </div>
  )
}

const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component' />, el);

You can see there that simply by having the state point to a reference, that doesn't change, and mutating the underlying values that the reference points to, you get the behaviour you're after both in the setInterval closure and in the React component.

Again, this is not idiomatic React, but just illustrates the point about references being the ultimate issue here. Hope it's helpful!

like image 34
davnicwil Avatar answered Oct 18 '22 03:10

davnicwil