DEV Community

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

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