Accessing up-to-date state from within a callback
Asked Answered
U

11

258

We 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 class components, 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.

With the following code, every time the callback accesses it, it's back at its default value. 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);

CodePen

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

CodePen

Does work. I need to call setState to access the previous state. Even though I have no intention of setting the state.

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

Upanchor answered 9/9, 2019 at 3:38 Comment(7)
I don't believe it's a duplicate of the above. As it's not about the fact setting the state is asynchronous. But about the state forever being stale in a callback. Now that I figured out the answer, it may however be a duplicate of https://mcmap.net/q/111285/-react-hooks-stale-state. However I think this points to an interesting consequence of how react hooks manage state. namely that you need to make a call to a function setting the state, in order to access the correct state within a callback. And that remains true even if you have no intention of changing the state.Upanchor
yes, you're right, it's about how function closure works. here is one of topics on that #57472487Hildredhildreth
Perhaps this blog from Dan Abramov would help: overreacted.io/making-setinterval-declarative-with-react-hooks, it explains why mixing hooks and setInterval is really confusing and doesn't seem to work at first. TL:DR basically because of closure you need to "re-enclose" the callback with the next state value.Guth
Yes I figured it was something along these lines as I would have bound this to the callback if I was using classes, but couldn't see how to do it with Effects. I tried approaches such as enclosing getters to the state variable as argument to the callback. But nothing worked. At any rate, after reviewing all the links everyone shared, something still isn't clear to me. Is there no way to just read the component's state from within another context than through calling its state-setting function (specifically when I do not want to change its state)?Upanchor
In some scenarios, wouldn't it be more correct to utilize the useReducer() hook for this? In my case, I have a callback passed to a library that is triggered on messages from Server Sent Events. When a message arrives to the client, the callback is triggered and pushes it to the existing messages state.Aniseed
I would really love to learn why exactly the state is not the latest in a callback. Ive had this exact issue a few times and while I can imagine why, some of the problems I had to solve with this method made no sense to me.Mathilda
See the useEvent proposal.Humphries
K
281

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.

Kingbolt answered 11/3, 2020 at 20:1 Comment(9)
Excellent answer. Thank you for this. I accepted it as it is the best fit for my scenario and I imagine the majority of real-world scenarios. For clarity to anyone else as to why I prefer this answer: although clearing and setting up a new callback with an updated state each time the state changes would be possible with the setInterval scenario in my example, it would not be easily done in scenarios where you cannot alter the callback of an ongoing request. Which has the be the majority of async libraries. Also this solution is simple and easy to understand.Upanchor
References must be mutated inside a side effect like useEffect. > avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects. it.reactjs.org/docs/…Cowen
IMO useRef does not really conform to the design philosophy of react, which is to avoid mutable state. This has been borne out by my experience: I have spent hours debugging bugs with state stored in mutable objects and not in the react state mechanism. I would encourage people (especially those new to react) to look at other options to learn to "think in react" more to solve problems like this.Chamber
@Chamber I do agre with you in principle. In this scenario however, I was dealing with an ongoing, long-running callback (provided to an external library) that needs to read the latest state. That in itself is not very React-like, but we may not have a choice. If we accept that we cannot remove and re-establish this callback, having the callback hold on to a mutable reference which "points" to the latest state seems coherent (more than having the callback use a setState to read the state, which just hides the issue). Of course, ideally we wouldn't be in this situation in the first place.Upanchor
I spent almost 3 hours to solve this. didn't think that it is this much simple. Thanks a lot for the answer.Silvery
@Upanchor did you find a more 'elegant' way to do this later on? I'm about to use this, I'm willing to find a more 'elegant' approach but I couldn't find one so far(I also happens to be a new to react)Bobker
@Bobker my conclusion was that useRef is the right approach if you need state to be readable by something outside of the React context and component lifecycle. It’s coherent (to me at least, others may disagree) to be passing a mutable reference to the state if the state needs to be accessed outside of React. However if you’re trying to read the latest state from within your React app, setState is a simple and good solution. But in the majority of such cases, you may not be doing things in a very React-like manner if this requirement arises. So it may be worth reviewing the overall approach.Upanchor
You are my saviorGrovel
Cannot add property current, object is not extensibleSociality
V
74

You can use the react-usestateref NPM package (disclosure: I am its creator), which creates a modified useState that implements the useRef approach in Brandon's answer. This useState is a compatible drop-in replacement for the standard React useState, that also gives you a mutable reference that always holds the current state.

You use it like this:

import useState from 'react-usestateref';

...

// inside the component:
const [count, setCount, countRef] = useState(0);

// countRef.current always gives you the latest state -- it's
// automatically updated whenever you call 'setCount'.

...

console.log(countRef.current);
setCount(20);
console.log(countRef.current);     // logs '20'

Another approach, instead of using useRef or modifying useState, is to use the fact that, if you call setState with a callback, it passes the current state value into that callback; so you can write:

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

useEffect(() => {
   var currentState;
   setState(currentState_ => {
      currentState = currentState_;
      return currentState_;  // don't actually change the state
   })
   alert(currentState); // the current state.
})
Verily answered 22/7, 2020 at 17:19 Comment(2)
I think this is perfect!. Instead of creating object instance and useRef hook. :)Villiers
is the syntax is correct in the e.g section?Travel
C
52

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!

Costard answered 18/6, 2020 at 15:31 Comment(4)
Thanks for this explanation! Totally clarified it for me.Kayekayla
But if the reference is still the same how React will know it's changed?Senarmontite
Great explanation. But to my opinion, React is guilty of this by going away from OO programming. If the state was explicitly exposed in a (mutable) object (the component), and not in a local variable on steroïd, such ugly side effects would not happen. Hope these guys will propose someday a programmer-friendly OO model with annotations (decorators).Kolyma
@Senarmontite think of a reference like a pointer to an object in memory- it will always be the same value because it's just the value of the address in memory, but the data it's pointing to is mutable and will therefore be accurate when read, even in the context of React's stateful memory model. This isn't the case with state variables because they are more like shallow copies of the value of a reference at a point in time. They are managed by React and therefore not guaranteed to be up to date.Internuncio
C
10

You can access the latest state in setState callback. But the intention is not clear, we never want to setState in this case, it may confuse other people when they read your code. So you may want to wrap it in another hook that can express what you want better

function useExtendedState<T>(initialState: T) {
  const [state, setState] = React.useState<T>(initialState);
  const getLatestState = () => {
    return new Promise<T>((resolve, reject) => {
      setState((s) => {
        resolve(s);
        return s;
      });
    });
  };

  return [state, setState, getLatestState] as const;
}

Usage

const [counter, setCounter, getCounter] = useExtendedState(0);

...

getCounter().then((counter) => /* ... */)

// you can also use await in async callback
const counter = await getCounter();

Live Demo

Edit GetLatestState

Charleen answered 2/9, 2020 at 10:49 Comment(3)
Beautiful abstraction, thank you for this example!Dehydrogenate
This is so cool! I really wished there was a proper "getter" function in React. The passive variable is enough for most situations, but for tricky situations like this, an active getter would have been so much better, solving the problem instantly. While your solution is very general and reusable across many state variables, I just used a simpler version where I call the default setter's callback overload to retrieve the value. I had to set a new value anyway, which I just returned in the setter callback, after processing the latest value.Caw
Also it's worth noting that the problem seems to occur for primitive types like boolean and number because these types get refactored into inline constants in the final JS code. That's why they never change. By using a callback function, you are basically preventing this simplification. The original problem doesn't seem to occur in objects which can change and be reflected correctly. (I'm not 100% sure about this though. I could be wrong.)Caw
O
5

Instead of trying to access the most recent state within a callback, use useEffect. Setting your state with the function returned from setState will not immediately update your value. The state updates are batched and updated

It may help if you think of useEffect() like setState's second parameter (from class based components).

If you want to do an operation with the most recent state, use useEffect() which will be hit when the state changes:

const {
  useState,
  useEffect
} = React;

function App() {
  const [count, setCount] = useState(0);
  const decrement = () => setCount(count-1);
  const increment = () => setCount(count+1);
  
  useEffect(() => {
    console.log("useEffect", count);
  }, [count]);
  console.log("render", count);
  
  return ( 
    <div className="App">
      <p>{count}</p> 
      <button onClick={decrement}>-</button> 
      <button onClick={increment}>+</button> 
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

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

Update

You can create a hook for your setInterval and call it like this:

const {
  useState,
  useEffect,
  useRef
} = React;

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
       savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}


function Card(title) {
  const [count, setCount] = useState(0);
  const callbackFunction = () => { 
    console.log(count);
  };
  useInterval(callbackFunction, 3000); 
  
  useEffect(()=>{
    console.log('Count has been updated!');
  }, [count]); 
  
  return (<div>
      Active count {count} <br/>
      <button onClick={()=>setCount(count+1)}>Increment</button>
    </div>); 
}

const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component'/>, el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

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

Some further info on useEffect()

Oporto answered 12/9, 2019 at 23:29 Comment(4)
Thanks for your answer @miroslav. I think I need to explain a bit more: The code I provided was just a toy example to illustrate the problem I've encountered. The issue I have is that an event fired from a third-party library needs to access the latest React state. To do so, I pass a callback to this library for it that can get the state. With react classes, I would have bound the callback to this - the component class. But with react effects, I have the unique problem that the state at the time of setting up the callback gets enclosed within the callback - leading to stale state.Upanchor
so far. The only solution I have found is to set the state (even though I have no desire to change the state), and use the state-setting function as a workaround to reading the state, setting the "new" state to being the same as the previous state (setupConsoleCallback(() => {setCount(prevCount => {console.log(`Count is: ${prevCount}`); return prevCount})}). But that really seems counter-intuitive. I wonder if it would also trigger an unecessary re-render to any effects that listed count as a dependency. Though I'm not too sure about that point.Upanchor
@Upanchor got the same issue, working with a callback to another library. Did you every figure out a way to do it other than setting the state to read the value?Beggary
@Beggary no I stuck with the approach I provided at the end of my question: using a setter function from the state hook to read the latest state (from the passed variable) without actually changing it. There may be a better way of doing this now, but at the time I believe this was the best approach.Upanchor
F
3

The only method i know is to call the setState(current_value => ...) function and use the current_value in ur logic. Just make sure you return it back. Ex:

const myPollingFunction = () => {
    setInterval(() => {
        setState(latest_value => {
            // do something with latest_value
            return latest_value;    
        }
    }, 1000);
};
Fortis answered 11/3, 2020 at 19:51 Comment(0)
E
3

I really like @davnicwil's answer and hopefully with the source code of useState, it might be more clear what he meant.

  // once when the component is mounted
  constructor(initialValue) {
    this.args = Object.freeze([initialValue, this.updater]);
  }

  // every time the Component is invoked
  update() {
    return this.args
  }

  // every time the setState is invoked
  updater(value) {
    this.args = Object.freeze([value, this.updater]);
    this.el.update()
  }

In the usage, if initialValue starts as a number or string, ex. 1.

  const Component = () => {
    const [state, setState] = useState(initialValue)
  }

Walkthrough

  1. first time you run useState, this.args = [1, ]
  2. second time you run useState, this.args no change
  3. if setState is invoked with 2, this.args = [2, ]
  4. next time you run useState, this.args no change

Now, if you do something especially with a deferred usage of the value.

  function doSomething(v) {
    // I need to wait for 10 seconds
    // within this period of time
    // UI has refreshed multiple times
    // I'm in step 4)
    console.log(v)
  }

  // Invoke this function in step 1)
  doSomething(value)

You will be given an "old" value, because you pass the current copy (at that time) to it at the first place. Although this.args gets the latest copy every time, it doesn't mean an old copy get changed. The value you passed isn't reference based. This could be a feature!!

Summary

In order to change it,

  • Use the value without passing it;
  • Use an object for the value;
  • Use useRef to get latest value;
  • Or design another hook.

Although the approach above all fix it (in other answers), the root cause of the problem is that you pass an old value to a function and expect it to run with a future value. I think this is where it went wrong at the first place, which isn't very clear if you just look at the solutions.

Excerpta answered 11/5, 2021 at 14:18 Comment(1)
honestly this answer clears it up better than the selected answer in my opinion. I was struggling with why this was happening in my application and pulling my hair out because of it. Is there any reason why the state variable being passed into a function parameter does not get updated when the component re-renders?Sunglass
U
1

I had similar problem but in my case I was using redux with hooks and also I was not mutating state in the same component. I had method which on callback used state value score and it (callback) had old score. So my sollution for that is quite simple. It's not as ellegant as previous ones but in my case it did the job so I'm putting it here with hope that it will help someone.

const score = useSelector((state) => state.score);
const scoreRef = useRef(score);

useEffect(() => {
    scoreRef.current = score ;
}, [score])

General idea is to store newest state in ref :)

Uriia answered 10/1, 2022 at 20:52 Comment(0)
W
1

I've written a hook that makes it easy to do this, as well as a hook meant specifically for keydown events which are a common case where this is run into: https://mcmap.net/q/111287/-referencing-outdated-state-in-react-useeffect-hook

The useEffectUnscoped hook is a generic implementation that stores an object in a ref (i.e. a state variable or a function that references a state variable that needs to be up to date).

useKeyDown is a cleaner hook meant more specifically for keydown events.

Wun answered 5/7, 2023 at 22:53 Comment(0)
Y
1

It's a common pattern in React. you have a callback (for exampler an event) and inside that callback you need to access updated value of state.
One way is to use useRef and probably an useEffect to keep useRef value up to date. But there is a better way: in the event just change one state (maybe an flag) and within useEffect put your main code:

function Card(title) {
    const [var, setVar] = useState(1);
    someElement.addEventListener('event',()=>{
        /*Some code should be here but we move the code
        Into useEffect and then trigger that useEffect*/
        setVar(2);
    });

    useEffect(()=>{
        /*The code you want to run in event should be here*/
    },[var]);

}
Y answered 14/7, 2023 at 12:0 Comment(0)
C
0

I would use a combination of setInterval() and useEffect().

  • setInterval() on its own is problematic, as it might pop after the component has been unmounted. In your toy example this is not a problem, but in the real world it's likely that your callback will want to mutate your component's state, and then it would be a problem.
  • useEffect() on its own isn't enough to cause something to happen in some period of time.
  • useRef() is really for those rare occasions where you need to break React's functional model because you have to work with some functionality that doesn't fit (e.g. focusing an input or something), and I would avoid it for situations like this.

Your example isn't doing anything very useful, and I'm not sure whether you care about how regular the timer pops are. So the simplest way of achieving roughly what you want using this technique is as follows:

import React from 'react';

const INTERVAL_FOR_TIMER_MS = 3000;

export function Card({ title }) {
  const [count, setCount] = React.useState(0)

  React.useEffect(
    () => {
      const intervalId = setInterval(
        () => console.log(`Count is ${count}`),
        INTERVAL_FOR_TIMER_MS,
      );
      return () => clearInterval(intervalId);
    },
    // you only want to restart the interval when count changes
    [count],
  );

  function clickHandler() {
    // I would also get in the habit of setting this way, which is safe
    // if the increment is called multiple times in the same callback
    setCount(num => num + 1);
  }

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

The caveat is that if the timer pops, then you click a second later, then the next log will be 4 seconds after the previous log because the timer is reset when you click.

If you want to solve that problem, then the best thing will probably be to use Date.now() to find the current time and use a new useState() to store the next pop time you want, and use setTimeout() instead of setInterval().

It's a bit more complicated as you have to store the next timer pop, but not too bad. Also that complexity can be abstracted by simply using a new function. So to sum up here's a safe "Reacty" way of starting a periodic timer using hooks.

import React from 'react';

const INTERVAL_FOR_TIMER_MS = 3000;

const useInterval = (func, period, deps) => {
  const [nextPopTime, setNextPopTime] = React.useState(
    Date.now() + period,
  );
  React.useEffect(() => {
    const timerId = setTimeout(
      () => {
        func();
        
        // setting nextPopTime will cause us to run the 
        // useEffect again and reschedule the timeout
        setNextPopTime(popTime => popTime + period);
      },
      Math.max(nextPopTime - Date.now(), 0),
    );
    return () => clearTimeout(timerId);
  }, [nextPopTime, ...deps]);
};

export function Card({ title }) {
  const [count, setCount] = React.useState(0);

  useInterval(
    () => console.log(`Count is ${count}`),
    INTERVAL_FOR_TIMER_MS,
    [count],
  );

  return (
    <div>
      Active count {count} <br/>
      <button onClick={() => setCount(num => num + 1)}>
        Increment
      </button>
    </div>
  );
}

And as long as you pass all the dependencies of the interval function in the deps array (exactly like with useEffect()), you can do whatever you like in the interval function (set state etc.) and be confident nothing will be out of date.

Chamber answered 20/6, 2020 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.