Skip to content

Question about getting the latest state value in the concurrent mode #20924

@hosseini44444

Description

@hosseini44444

React version: 17.0.1

Steps To Reproduce

  1. Enable strict mode for checking for possible issues in the future concurrent mode
  2. create the below component and run the code
import { useCallback, useState } from "react";

const Example = ({ onIncrement }) => {
  const [count, setCount] = useState(0);

  const incrementHandler = useCallback(() => {
    onIncrement(count, count + 1);  // Is count guaranteed to be the latest state here due to including count in the useCallback dependency array?
    setCount((count) => count + 1);
  }, [count, onIncrement]);

  return (
    <>
      <span>{count}</span>
      <button onClick={incrementHandler}>increment</button>
    </>
  );
};

const Parent = () => (
  <Example
    onIncrement={(currentCount, incrementedCount) =>
      alert(
        `count before incrementing: ${currentCount}, after increment: ${incrementedCount}`
      )
    }
  />
);

export default Parent;

The current behavior

In this simple example everything seems to be fine but in a more complicated situation full of event handlers that change the count or async callbacks that may change the count( like data fetching callbacks) the count value is not guaranteed to be the latest state and if I change the incrementHandler function like below:

const incrementHandler = useCallback(() => {
    setCount((count) => {
      onIncrement(count, count + 1);  
      return count + 1
    });
  }, [count, onIncrement]);

then the onIncrement will run twice in development while in strict mode and may run twice in production in concurrent mode according to documentation.
and If you suggest running the onIncrement in useEffect callback with count and onIncrement in effect's dependencies array how can I know that the onClick event of the increment button has caused the effect and not another event for example decrement or anything else.

you may say by setting another state that shows what is responsible for the effect, then I may need the previous state which unlike this example may be impossible to recalculate.

you may suggest using a ref for storing the previous state (count) then I will end up with one extra state or ref for storing what is responsible for the effect to run, one extra ref for storing the previous state, and a useEffect hook to run the onIncrement click event handler

The expected behavior

Providing a second callback argument to setState like in class Components that will run after this state update so we can save the current and next state and use it in the callback like below:

const incrementHandler = useCallback(() => {
    let prevCount, nextCount;
    setCount(
      (count) => {
        prevCount = count;
        nextCount = count + 1;
        return nextCount;
      },
      () => onIncrement(prevCount, nextCount)
    );
  }, [onIncrement]);

In my humble opinion, this doesn't collide with the async nature of setCount and can be implemented.

unlike belowgetState proposals that if it will be asynchronous it may not return the desired state. and if it will be synchronous it will not return the latest state too because setState is not executed yet.

wrong solution:

const [count, setCount, getCount] = useState(0);

  const incrementHandler = useCallback(() => {
    setCount((count) => count + 1);
    const currentCount = getCount();
    const nextCount = currentCount + 1;
    onIncrement(currentCount, nextCount)
  }, [onIncrement]);

or providing a third array to useCallback for accessing the latest state can not be implemented due to the same problem with getState and async nature of setState.

Please tell me if I'm missing something or I've misunderstood things.

If not, please tell me if there is a simple solution for this scenario or similar ones, or tell me the best practices for running a callback or event handler with the latest state.

Thank you!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions