As far as I am to understand, reducers do not do anything in react outside of being syntactic sugar for useState. But it doesn't actually work differently under the hood. For example, lets say I'm trying to create a custom hook:
const update = (state, action) => {
switch (action) {
case 'increment':
return state + 1
break
case 'decrement':
return state - 1
break
default:
break
}
}
const initState = 0
const useWithReducer = () => {
const [state, dispatch] = useReducer(update, initState)
return [state, dispatch]
}
const useWithState = () => {
const [state, setState] = useState(initState)
const dispatch = (action) => setState(update(state, action))
return [state, dispatch]
}
These two things are the same.
As far as I can understand, it doesn't matter how complicated initState
or update()
get, the reducer and state versions should always work the same. If I update initState
to be {nestedState: 0}
and updated update accordingly it would still work. In other words useWithReducer, and useWithState are technically the same/doing the same thing. Is this correct? Is there something I'm missing about how either of these items work under the hood? The only thing I could think of for why useState
might be better/different would be something like:
const useWithState = () => {
const [state, setState] = useState(initState)
const dispatch = (action) => setState((currentState) => update(currentState, action))
return [state, dispatch]
}
As far as I am to understand, reducers do not do anything in react outside of being syntactic sugar for useState. But it doesn't actually work differently under the hood.
Well, i would actually say it's the other way around. useReducer
is the core functionality, and useState
is a convenience function that wraps around that.
Looking at the source code, on the first render,useState
creates a dispatch function, which is what gets returned as index 1 (and then often assigned a name like setFoo
by the user's code):
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
And then on subsequent renders, useState just runs the code for useReducer:
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
Your code does look like it reproduces most of the behavior of useReducer. The main difference i see in your code from react's code is that you create a new dispatch function on every render, while react only does it once. This could break memoization unexpectedly. And as you point out in the last example you would also need to make sure to be operating on the most recent state, which react does out of the box.