1

Here, it is mentioned that "can you trust React to update the state in the same order as setState is called for ... Yes". My question is that are dispatch (useReducer) events also run in the same order as they are called? For example, consider something like this:

const [states, dispatch] = useReducer(reducer, initialState)
dispatch('a')
dispatch('b')

Can I be sure that all the logic inside the reducer function is executed with parameter 'a' and the it is executed with parameter 'b' (the call with parameter 'b' uses the state modified by the first call)?

MODIFIED: And what about reducers and setStates combined? Is their order of calls also kept? e.g., a setState, a reducer (which uses the value of the state set in the setState).

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Shayan
  • 2,758
  • 7
  • 36
  • 55

2 Answers2

0

Yes, reducers are pure functions that run synchronously, so any single dispatched action is processed before the next.

This may help you better understand reducer functions: https://redux.js.org/basics/data-flow

The functional component body is completely synchronous, so everything is process in the order it is called per render cycle. The link is from redux, but the reducers work the same, and are interchangeable, i.e. reducer function has signature (state, action) => nextState.

All hook values work between render cycles, meaning, all the state updates and dispatched actions "queued" up during a render cycle are all batch processed in that order.

Given:

dispatch('a')
dispatch('b')

The dispatch('b') reducer case will run after dispatch('a') case has completed.

Update

You can review the source code of both the useState and useReducer hooks and see that they will process updates in the same synchronous, sequential manner.

Dispatch

type Dispatch<A> = A => void;

Here you can see that dispatch is a synchronous function.

useState

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useReducer

export function useReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

Both useState and useReducer use the same internal dispatch system (i.e. resolveDispatcher). Since useReducer can't handle asynchronous actions (i.e. like Redux-Thunks) there is no other option but to synchronously handle actions in the reducer before handling the next dispatched action.

Update 2

MODIFIED: And what about reducers and setStates combined? Is their order of calls also kept? e.g., a setState, a reducer (which uses the value of the state set in the setState).

The order will be maintained, but you won't be able to enqueue a state update and then dispatch an action based on that updated state. This is due to the way React state updates are asynchronously processed between render cycles. In other words, any setState(newState) won't be accessible until the next render cycle.

Example:

const [countA, setCountA] = useState(0);
const [countB, dispatch] = useReducer(.....);

...

setCountA(c => c + 1); // enqueues countA + 1, next countA is 1
dispatch(setCountB(countA)); // dispatches with countA === 0
setCountA(c => c + 1); // enqueues countA + 1, next countA is 2
dispatch(setCountB(countA)); // dispatches with countA === 0
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • And what about reducers and setStates? Is their order of calls also kept? e.g., a setState, a reducer (which uses the value of the state set in the setState). – Shayan Jun 06 '20 at 07:05
  • The link you provided is related to redux. My question is about React. – Shayan Jun 06 '20 at 07:08
  • Any functions using any state values will all use the state value from the current render cycle. The functional component body is completely synchronous, so everything is process in the order it is called per render cycle. Yes, it is to redux, but the reducers work the same, and are interchangeable, i.e. reducer function has signature `(state, action) => nextState`. Can you clarify more what you mean when you ask about "reducers and setStates combined"? – Drew Reese Jun 06 '20 at 07:09
  • It seems that the orders of setStates are kept. Also, the orders of dispatches are kept. Are also the orders between setStates and dispatches kept? – Shayan Jun 06 '20 at 10:52
  • @Shayan I believe that is irrelevant between the two as all updates from a render cycle will use the current state value, meaning *any* dispatched actions that include any state values will use values from the current state of *that* render cycle, so whether or not you call `setState` then `dispatch`, or `dispatch` then `setState`, the outcome will be the same. The state set will be available *on the next* render cycle, same as the `nextState` from a reducer from a dispatched action. – Drew Reese Jun 06 '20 at 21:43
-1

The behavior I am seeing is that dispatch in react is NOT synchronous, despite insistence from many. The reducer is synchronous, but there does not appear to be a guarantee that the completion of dispatch guarantees that the resulting reducer has completed. This many be different in redux, but if we're talking React at least in 17.0.2.

Here is an example test case:

import { useReducer } from "react";
import "./styles.css";

function reducer(state, action) {
  console.log("REDUCER", state, action);
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    default:
      return state;
  }
}

export default function App() {
  const [state, dispatch] = useReducer(reducer, 0);

  const myDispatch = function (...args) {
    console.log("dispatched", args);
    return dispatch(...args);
  };

  const onClick = function () {
    // BOTH dispatches fired before any reducer!
    myDispatch({ type: "INCREMENT" });
    myDispatch({ type: "INCREMENT" });
  };

  const onClickAsync = async function () {
    // reducer finishes before next dispatch called
    await myDispatch({ type: "INCREMENT" });
    await myDispatch({ type: "INCREMENT" });
  };

  return (
    <>
      <pre>{state}</pre>
      <button onClick={onClick}>Increment Broken</button>
      <button onClick={onClickAsync}>Increment Working</button>
    </>
  );
}

Sandbox: https://codesandbox.io/s/dispatch-not-sync-db9wn?file=/src/App.tsx

In that example if you click on the Increment Broken button I execute dispatch one after the other and you'll see in the console that the log statements for BOTH dispatches show up prior to any reducer calls.

On the other hand if you click on the Increment Working button you will see that it correctly does dispatch -> reducer -> dispatch -> reducer.

The execution of the reducers is guaranteed to be in the order queued up, and the state received by the second reducer is indeed the state of the previous reducer, correctly. So basically this only matters if the queuing of the second dispatch depends upon the reducer being completed prior to starting the next dispatch.

Owen Allen
  • 11,348
  • 9
  • 51
  • 63
  • I see the same behavior when clicking either button, i.e. the logged output is "dispatch, reducer, dispatch, reducer". I'm not sure what point you are trying to make as your `myDispatch` function is neither declared `async` nor does it return a Promise, so you can't `await` it as there's nothing to wait for. All the code, other than the `onClickAsync` is completely synchronous. OP's question was about the order the dispatches are processed and your code proves and agrees with my answer that they are processed in the order they are dispatched. – Drew Reese Sep 17 '21 at 18:44
  • Ah, it just occurred to me why you are seeing the behavior you see in your sandbox. The updates are not batched since you are enqueueing them in an asynchronous callback. See this [answer](https://stackoverflow.com/questions/53048495/does-react-batch-state-update-functions-when-using-hooks) with some explanations regarding React and batched/non-batched updates. – Drew Reese Sep 17 '21 at 19:24