DEV Community

loading...
Cover image for React hooks: get the current state, back to the future

React hooks: get the current state, back to the future

scastiel profile image Sébastien Castiel Originally published at blog.castiel.me Updated on ・3 min read

This article is not recent, but still relevant! And I still often see developers having trouble with this concern. I talk about this topic and a lot more about React hooks in my new eBook A React Developer’s Guide to Hooks.

React Hooks are trully awesome, but the more I play with them the more I discover tricks, and sometimes spend a lot of time figuring out why my code doesn’t do what it is supposed to.

My last problem was this one: I wanted to access the current state (created with useState) of a component, from a callback triggered asynchronously, in useEffect or useCallback for instance.

Here is an example of code that doesn’t work as you might expect:

const Counter = () => {
  const [counter, setCounter] = useState(0)
  const onButtonClick = useCallback(() => setCounter(counter + 1), [counter])

  const onAlertButtonClick = useCallback(
    () => {
      setTimeout(() => {
        alert('Value: ' + counter)
      }, 5000)
    },
    [counter]
  )

  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={onButtonClick}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 5 seconds
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

You may recognize the counter example extracted from React documentation, to which I added a new button. When this button is clicked, an alert is shown five seconds later, with the current value of the counter. Or that’s what you could imagine, unfortunately the displayed value is not the current one.

Let’s say you click the button when the counter is 5, then immediately after you click the increment button three times. You expect the alert to display 8, yet it displays 5. This is because in the function given to setTimeout, counter’s value is 5, and there is no reason for it to be updated (React hooks are not that magical). It’s plain JavaScript closure and scope concern, so obviously we need to find another way to do what we want.

The answer: refs and the hook useRef. The idea is to use a ref for the counter; it would be updated it each time counter is, and we would use its current value in the function given to setTimeout.

So first we declare our ref, with current counter value as initial value:

const counterRef = useRef(counter)
Enter fullscreen mode Exit fullscreen mode

Then we want to update it every time counter is updated, so we can use useEffect:

useEffect(
  () => { counterRef.current = counter },
  [counter]
)
Enter fullscreen mode Exit fullscreen mode

Finally, we only have to use counterRef.current in out timeout function:

const onAlertButtonClick = useCallback(() => {
  setTimeout(() => {
    alert('Value: ' + counterRef.current)
  }, 5000)
}, [])
Enter fullscreen mode Exit fullscreen mode

Note: I think it’s not necessary to give [counter] as second parameter, as counterRef should not change between renderings.

This works very well! And we can even create a custom hook to make this process simpler and reusable:

const useRefState = initialValue => {
  const [state, setState] = useState(initialValue)
  const stateRef = useRef(state)
  useEffect(
    () => { stateRef.current = state },
    [state]
  )
  return [state, stateRef, setState]
}
Enter fullscreen mode Exit fullscreen mode

Our component code is then much simplified:

const Counter = () => {
  const [counter, counterRef, setCounter] = useRefState(0)
  const onButtonClick = useCallback(() => setCounter(counter + 1), [counter])

  const onAlertButtonClick = useCallback(() => {
    setTimeout(() => {
      alert('Value: ' + counterRef.current)
    }, 5000)
  }, [])

  return (
    <div>
      <p>You clicked {counter} times.</p>
      <button onClick={onButtonClick}>Click me</button>
      <button onClick={onAlertButtonClick}>
        Show me the value in 5 seconds
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

I’m not fully sure this is the best way to address this concern of getting a state value in the future, although it seems to work fine. Were you confronted to the same kind of issue with state and hooks? Do you see another way to do, or any issue with this one?

This article was originally posted on my blog. Photo by Sergey Zolkin on Unsplash.

Discussion (7)

Collapse
dan_abramov profile image
Dan Abramov

So this might seem unexpected in this case. But let's consider an equivalent case that looks a bit more concrete than a counter.

handleFollowClick() {
  setTimeout(() => {
    alert('Followed: ' + this.props.user.name);
  }, 5000)
}
Enter fullscreen mode Exit fullscreen mode

Do you see the bug in this code? If the user prop changes before the timeout finishes (e.g. because you navigated to a different profile), you would "follow" the wrong person!

How would you fix it? With a class, one way to fix it is to destructure early:

handleFollowClick() {
  const {user} = this.props;
  setTimeout(() => {
    alert('Followed: ' + user.name);
  }, 5000)
}
Enter fullscreen mode Exit fullscreen mode

Many people wouldn't notice the difference, but this code takes care to remember which user you referred to at the time of the click.

So how does this relate to Hooks? With Hooks, the code behaves like the second snippet by default. You always get the values in the event handlers that were actually rendered.

function handleFollowClick() {
  // There is no "this", the "user" is always
  // the one corresponding to this event handler.
  setTimeout(() => {
    alert('Followed: ' + user.name);
  }, 5000)
}
Enter fullscreen mode Exit fullscreen mode

As you described, a ref is a way to "escape" that snapshot time in time and peek into the future. In many cases you don't want it; but it's there for when you need it. It takes time to adjust to a new default though.

Collapse
scastiel profile image
Sébastien Castiel Author

Thanks Dan, it kind of confirms (and explains very well) what I suspected 🙂

Collapse
dan_abramov profile image
Dan Abramov

I wrote up a more detailed explanation. overreacted.io/how-are-function-co...

Collapse
dance2die profile image
Sung M. Kim

Great post, Sébastien~

The problem statement & the steps to get the "current" prop (not the closed prop) was fun to read 🙂

In case anyone wants a runnable code, here it is.

Collapse
stavares profile image
Scott C Tavares

I have been writing OO code in Java for nearly three decades, I have been writing functional code for about three months now so excuse me if this sounds like I'm just ignorant of a few things. My first question is, isn't it functional programming's goal to not maintain state information between method calls? Second is, if you combine this with TypeScript, don't you now just have plain ol' Java again?

Forem Open with the Forem app