Explanation#
- This article is based on v18.1.0.
- To read this article, you need to read React Hooks: hooks chain and React Hooks: useState analysis first.
- Like other hooks, it is divided into mount and update phases.
- It is almost identical to useState, so many contents can be directly referenced from useState.
- The analysis is based on the official demo.
import { useReducer } from "react";
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
export default Counter;
useReducer in the mount phase#
When initializing useReducer, it will enter
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// Create a hook object and attach it to the linked list
const hook = mountWorkInProgressHook();
let initialState;
// Check if there is an initialization function
if (init !== undefined) {
// Process initialArg based on init
initialState = init(initialArg);
} else {
// Assign directly
initialState = ((initialArg: any): S);
}
// Mount the initial value to the hook object
hook.memoizedState = hook.baseState = initialState;
// Like useState, they both have a queue to manage updates
// You can refer to the analysis of useState for details, skipping here
const queue: UpdateQueue<S, A> = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
/**
* Like useState, dispatch is thrown out, but here it is dispatchReducerAction, useState throws
* dispatchSetState, which is similar, let's take a look next
*/
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
As you can see, the implementation of useReducer is almost identical to useState in the mount phase, and it is almost the same later.
Triggering dispatch of useReducer#
After clicking on any onClick, it will enter dispatchReducerAction.
Since it is too similar to dispatchSetState, only the differences will be explained here.
function dispatchReducerAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
...
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
/**
* Compared with dispatchSetState, it can be found that
* dispatchReducerAction does not calculate the expected value (eagerState) for the first generated update
*/
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
...
}
The rest is exactly the same.
useReducer in the update phase#
The update phase of useReducer is exactly the same as useState, which has been explained in useState, so it will not be repeated here.
Summary#
The implementation logic of useReducer is almost identical to useState. The only two differences are:
- The dispatch of useReducer does not calculate the expected value (eagerState) when generating the first update object.
- useReducer does not generate a basicStateReducer like useState in the mount phase, but directly uses the passed Reducer.