DEV Community

Cover image for Stale Closures in React.useEffect() Hook "A Weird Bug for New React Learners"
Prashant Jha
Prashant Jha

Posted on

Stale Closures in React.useEffect() Hook "A Weird Bug for New React Learners"

It was a beautiful day, my day was going almost good but suddenly...
I encountered stale closures in React.useEffect()

This was my code, ahh... a simple and small code but good enough to frustrate you.

Stale Closure Bug

When you run this code, It will show

Hello Bucky

on screen.

Haha, Nah It'll not

The thing is it'll only show Hello + "", I mean only Hello.
Why?
because of closures.
Hey, what is closures?
Checkout this video

Okay got it? That video on closure was amazing isn't it...
Now let's continue.

Here are two ways you can solve this problem

The useEffect's dependency array
&
Using a ref instead of a useState

The useEffect's dependency array

Closure issue solve using useEffect's dependency array

On line 15 just put user in dependency array, so from now whenever user value will get updated the useEffect() will render again and new value of user will get set in line 14.

But, this approach will set greeting twice one with previous("") user value and one with updated("Bucky") user value as useEffect() will render twice.

Let's look into another better solution to tackle this stale closures issue.

Using a ref instead of a useState

Closure issue solve using a ref instead of a useState

What is useRef()?

As, in previous approach useEffect() was rendering twice, but in this useEffect() will only run once.

If you are thinking which is better, I believe it's depend more upon on your use case.

Cool... Now It's end, if you're having any suggestion in this article please update me in comment, I'll love to update this article with more better information. Thankyou Have a great day.

Thankyou, bye

Top comments (4)

Collapse
 
peerreynders profile image
peerreynders • Edited

Let's look into another better solution to tackle this stale closures issue.

Don't use the closure value for initialization but initialize both states from the same fresh value instead.


Once asynchronous updates get involved things get even more interesting in terms of the number of renders:

Two renders:

import { useEffect, useState } from 'react';
// React 17.0.2 (no batching of asynchronous updates)

// Emulate a fetch
const getUserNames = () => {
  const executor = (resolve, _reject) => {
    const reply = () => resolve(['Prashant', 'Sunny', 'Bucky']);
    setTimeout(reply, 500);
  };
  return new Promise(executor);
};

export default function App() {
  // render #1: initialization to empty
  const [user, setUser] = useState('');

  useEffect(() => {
    async function fetchData() {
      const userNames = await getUserNames();
      const name = userNames[2];
      // trigger render #2: setUser
      setUser(name);
    }
    fetchData();
  }, []);

  return <h1>{`Hello ${user}`}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Four renders:

export default function App() {
  // render #1: initialization
  const [user, setUser] = useState('');
  const [greeting, setGreeting] = useState('');

  useEffect(() => {
    async function fetchData() {
      const userNames = await getUserNames();
      const name = userNames[2];
      // trigger render #3: setUser
      setUser(name);
    }
    fetchData();
  }, []);

  useEffect(() => {
    // trigger render #2 with user = ''
    // trigger render #4 with user = 'Bucky'
    setGreeting(`Hello ${user}`);
  }, [user]);
  return <h1>{ greeting }</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Three renders:

export default function App() {
  // render #1: initialization
  const [_user, setUser] = useState('');
  const [greeting, setGreeting] = useState('');

  useEffect(() => {
    async function fetchData() {
      const userNames = await getUserNames();
      const name = userNames[2];
      // trigger render #2: setUser
      setUser(name);
      // trigger render #3: setGreeting
      // i.e. asynchronous `setState`s are not batched
      setGreeting(`Hello ${name}`);
    }

    fetchData();
  }, []);

  return <h1>{greeting}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Back to two renders:

export default function App() {
  // render #1: initialization
  const [greeting, setGreeting] = useState('');

  useEffect(() => {
    async function fetchData() {
      const userNames = await getUserNames();
      const name = userNames[2];
      // trigger render #2: setGreeting
      setGreeting(`Hello ${name}`);
    }

    fetchData();
  }, []);

  return <h1>{greeting}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

Picking useRef() over useState() comes down to:

  • Don't want to re-render when value.current is rebound to a new value - then use useRef() (Aside: another use of useRef() that doesn't reference a DOM element: handleEvent & React )
  • If value can change (e.g. via useEffect()) requiring a re-render then useState() has to be used.

Deep dive: How do React hooks really work?


Major edit: Made implied fetch explicit by emulating it with a promise.

Collapse
 
pbucky profile image
Prashant Jha

Thanks for explanation

Collapse
 
_genjudev profile image
Larson

The more correct solution would be this:

import React from "react"

const getUserNames = () => ["Prashant", "Sunny", "Bucky"]

export default function App() {
    return <h1>{`Hello ${getUserNames()[2]}`}</h1>
}
Enter fullscreen mode Exit fullscreen mode

The setState just sets the state for the next render.
React is about simplicity. And you have no benefit in use useRef like this.

A better example for using useRef would be for the main purpose:
reactjs.org/docs/hooks-reference.h...

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pbucky profile image
Prashant Jha

github.com/facebook/react/issues/1... You can read this also.