0

Consider the following setup:

const [state, dispatch] = useReducer((_state, action) => _state + action, 0);

return <div>{state}</div>

With the above code, if I fire the dispatcher with dispatch(2), the render will reflect '2' and if I later fire the dispatch with dispatch(3), the render will reflect '5' as expected. No issues there.

However, if I were to use the following code instead:

const [state, dispatch] = useReducer((_state, action) => _state + action, 0);

useEffect(() => {
  dispatch(2);
  dispatch(3);
}, []);

return <div>{state}</div>

Then the final render would only be '3'.

The useReducer hook does not actually update your state until all actions are finished processing, which is fine that is very desirable behaviour. The part that is strange to me is that it should at least 'update' the state used within the reducer internally so that any future actions in that same execution thread are aware of changes made to the state by the actions that came before it.

Otherwise, any scenario where you have more than one action in the same event sequence will cause your reducer to yield an incorrect result and make useReducer useless.

I cannot for the life of me understand why they wrote it this way? Is this a bug? Or am I missing something here?

For now I have solved the problem by using my own useReducer hook:

queuedReducerHook.js:

import { useReducer, useRef } from 'react';

const useQueuedReducer = (reducer, initialValue) => {
  const liveStateRef = useRef(initialValue);
  
  return useReducer((outOfDateState, action) => {
    liveStateRef.current = reducer(liveStateRef.current, action);
    
    return liveStateRef.current;
  }, initialValue);
};

export default useQueuedReducer;

Update

What's probably more confusing now is that after using the solution above, I noticed that React was sometimes duplicating action dispatches (or entire sequences of them, as long as they all arrived in the same execution thread). I tried for days to solve this, but just couldn't In the end, the only thing that worked for me was to swap out the useReducer entirely. I ended up using my own implementation and suddenly all my problems went away!

The implementation I used (does not rely on React's useReducer)

import {useCallback, useState, useRef} from 'react';

const useQueuedReducer = (reducer, initialValue) => {
  const [state, setState] = useState(initialValue);
  const liveStateRef = useRef(initialValue);
  const reducerRef = useRef();

  reducerRef.current = reducer;
  return [
    state, 
    useCallback((action) => {
      const newState = reducerRef.current(liveStateRef.current, action);
      setState(newState);
      liveStateRef.current = newState;
    }, []),
  ];
};

export default useQueuedReducer;
user2765977
  • 491
  • 4
  • 18

0 Answers0