skip to Main Content

I haven’t found any similar implementations, so I’m wondering if this is a valid thing to do in React?

Problems with useState

  1. It’s prone to stale value (which can happen in non-obvious ways). Example codepan. Note the oddCounter is 0, because counter is stale.

  2. It can’t use async functions.

Workaround

This workaround uses only the setState function.

// Mutex
class Mutex {
  // Implementation omitted.  These can be easily implemented with an instance field of Promise that's resolved in unlock.
  lock = async () => {...};
  unlock = () => {...};
}

// Component
const MyComponent = () => {
  const [counter, setCounter] = useState(0);

  const mutex = useRef(new Mutex());
  const handler = useCallback(async () => {
    await mutex.lock();

    let curCounter = await new Promise((res, rej) => {
      // Use setCounter as a "probe" to get the current counter.
      setCounter((v) => {
        res(v);
        return v;
      }
    });

    // Note: we can use async.
    curCounter += await getDelta();
    setCounter(curCounter);
    mutex.unlock();
  }, []); // Note dep list is empty, because this doesn't depend on counter

  return (<Button onClick={handler}>Click</Button>);
}

This may seem contrived, but the logic can be abstracted into a reusabled high-order function with "setState" as input.

This solves both the stale variable (we don’t rely on "counter") and also allows using async.

There’re some caveats with this approach:

  1. Must use a mutex to prevent multiple function calls at the same time, otherwise curCounter might have race condition. This may incur a performance hit.
  2. Might need to deal with re-entrant locks.
  3. Must remember to unlock, otherwise program will hang.

2

Answers


  1. Option 1 – oddCounter is derived from counter. Instead of using another state, recompute it on the fly. You can memoize it if the component re-renders due the changes other than the counter.

    Note: +(counter % 2 === 1) evaluates to 0 (false) or 1 (true).

    increment = React.useCallback(() => {
      setCounter(v => v + 1);
    }, []);
    
    const oddCounter = useMemo(() => counter.reduce(
      (sum, n) => sum + +(counter % 2 === 1), 0)
    , [counter]);
    

    Option 2 – use a useEffect to react to counter changes, and update oddCounter:

    increment = React.useCallback(() => {
      setCounter(v => v + 1);
    }, []);
    
    useEffect(() => {
      if (counter % 2 === 1) {
        setOddCounter(v => v + 1);
      }
    }, [counter]);
    
    Login or Signup to reply.
  2. You can call setOddCounter within the call to setCounter, and then increment doesn’t depend on either counter, so it never becomes stale.

    increment = React.useCallback(() => {
        setCounter((v) => {
          if (v % 2 === 1) {
            setOddCounter((o) => o + 1);
          }
          return v + 1; 
        });
      }, []);
    

    This doesn’t let you use async, but that only matters if the previous state is required for the async action.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search