2

I have a component that renders a table with objects. This component shows a button that, when pressed, sends the parent a specific object. The parent must set it in the state to display some graphical stuff. The rendering is working correctly, what I don't understand is why I am getting an outdated value after setting the state correctly. It's not a race condition, React is simply ignoring the updated value of a variable, even when it re-renders the component correctly.

A minimal example:

import { useState } from "react";
import { SomeComponent } from "./SomeComponent";

export default function App() {
  const [currentID, setCurrentID] = useState(null);

  function getData() {
    console.log("Getting data of: ", currentID); // PROBLEM: this is null
  }

  function setAndRetrieveData(value) {
    setCurrentID(value);

    // Just to show the problem and discard race conditions.
    setTimeout(() => {
      getData();
    }, 1500);
  }

  return (
    <div className="App">
      <h1>Current ID: {currentID}</h1> {/* This works fine */}
      <SomeComponent getInfoFor={setAndRetrieveData} />
    </div>
  );
}

SomeComponent:

export function SomeComponent(props) {
  const randomID = 45;

  return <button onClick={() => props.getInfoFor(randomID)}>Get info</button>;
}

Edit react update state

Even with solutions like useStateCallback the problem persists.

Is there a way to do this without having to use the awful useEffect which is not clear when reading the code? Because the logic of the system is "when this button is pressed, make a request to obtain the information", using the hook useEffect the logic becomes "when the value of currentID changes make a request", if at some point I want to change the state of that variable and perform another action that is not to obtain the data from the server then I will be in trouble.

Thanks in advance

Genarito
  • 3,027
  • 5
  • 27
  • 53
  • 1
    `setTimeout` is a `closure`, therefore, when `setTimeout` is scheduled it uses the value at that exact moment in time, which is the initial value To solve this, use the `useRef` Hook: – Ayan Mehta Jul 12 '22 at 19:02
  • 1
    you can refer this discussion and for this solution of `useref ` https://github.com/facebook/react/issues/14010 – Ayan Mehta Jul 12 '22 at 19:05
  • Thank you for the reference! I'll read it carefully – Genarito Jul 12 '22 at 19:52

2 Answers2

3

I think this is an issue with the way Javascript closures work.

When you execute a function, it gets bundled with all the data that pertains to it and then gets executed.

The issue is that you call this:

setTimeout(() => {
      getData();
    }, 1500);

inside setAndRetrieveData(value).

Even though it's inside a setTimeout, the getData() function has been bundled with the information it needs (currentID) at that point in time, not when it actually runs. So it gets bundled with the currentId before the state update takes place

Unfortunately, I would recommend using useEffect. This is the best way to ensure you avoid issues like this and any potential race conditions. Hopefully someone else can provide a different approach!

Pelayo Martinez
  • 287
  • 1
  • 8
  • 1
    Effectively, that's the problem. I fixed it by verifying the state of `currentID` and a lot of more variables in a `useEffect` callback. Now my code it's dirtier and less maintainable, but the bug disappeared. Thank you very much for your answer! – Genarito Jul 12 '22 at 19:54
1

when setAndRetrieveData is called it sets a state that leads to the component being rerendered to reflect the new state. When the timeout finishes The function getData was created in the previous render. And thus only has access to the state variable from the previous render. That now is undefined.

what you could try is using a useEffect hook that that listens to changes of currentID.

   useEffect(() => {
      const timeoutId = setTimeout(() => {
         // Do something with the updated value
      },1000);
      return () => {
          // if the data updates prematurely 
          // we cancel the timeout and start a new one
          clearTimeout(timeoutId);
      }
   },[currentID])
  • I'll definitely go back to React classes, `useEffect` fixed the problem, but It makes the code a lot more verbose and unclear. Thank you very much for your answer! – Genarito Jul 12 '22 at 19:56