The value of useReducer (proposal)
Hi!
This is not a bug.
Please help me to understand the value of useReducer.
Let's take a look at the typical example:
import React, { useReducer } from 'react';
interface CounterState {
count: number;
}
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' };
const initialState: CounterState = { count: 0 };
const reducer = (state: CounterState, action: CounterAction): CounterState => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unhandled action');
}
};
const Counter: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
Documentation says: https://react.dev/learn/extracting-state-logic-into-a-reducer#comparing-usestate-and-usereducer
However, useReducer can help cut down on the code if many event handlers modify state in a similar way.
useReduceris usually preferable touseStatewhen you have complex state logic that involves multiple sub-values. It also lets you optimize performance for components that trigger deep updates because you can passdispatchdown instead of callbacks.
Wouldn't it be easier to use just a JavaScript module to aggregate state logic?
const myModule = {
increment: (state) => {
return {count: state.count + 1};
},
decrement: (state) => {
return {count: state.count - 1};
}
};
const Counter: React.FC = () => {
const [state, setState] = useState(initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => setState(myModule.increment)}>Increment</button>
<button onClick={() => setState(myModule.decrement))}>Decrement</button>
</div>
);
}
Benefits:
- no calling function by string
- no switch statement
- no conditional types (CounterAction)
Hi,
useReducer is beneficial when dealing with complex state logic and multiple sub-values; a very deep object for example. As the documentation says, it makes testing and debugging easier. So for small and simple states useState does the job. If you're incrementing the complexity of your state you should start thinking in useReducer
Anyway I believe this sort of questions have a better place: https://react.dev/community
useReducer is beneficial when dealing with complex state logic and multiple sub-values; a very deep object for example. As the documentation says, it makes testing and debugging easier.
I'm trying to convey that for the described use case, useState does a better job than useReducer. Please see my example in the first message.
My proposal is to deprecate useReducer and provide more examples for useState.
I mean, yeah got your point. What I tried to say was that your concrete use case was simple enough to use useState instead of useReducer. No need to deprecate it; just don't use it if you don't need it. Both hooks serve different purposes. As you can see in the source code, they do work in a similar way but with a couple of differences.
You may want to check the documentation for more useReducer examples in order to catch the idea of its purpose:
- https://react.dev/reference/react/useReducer (This one includes an example with
Immer) - https://legacy.reactjs.org/docs/hooks-reference.html#usereducer (this one has very practical examples regarding lazy initialization and the combo with
useContext)
Regarding to "...provide more examples for useState". It's up to you how you're going to use useState, I'm sure there're a lot of cool recipes out there (including yours). I mean you can make an API request in the initial state; doesn't mean you should do it tho ¯_(ツ)_/¯.
Conside if you are building an ecommerce site or something more complex, at that point of time the useReducer will be of more use than useState. If you consider a simple increment, decrement separate module will be better option. Ensuring that the state remains consistent and predictable across all actions and components can be difficult with a separate module, especially as the application grows and evolves over time
If we were going to remove anything, it would be useState, because useState is basically a helper for useReducer to handle the basic cases. In fact, useState is implemented with useReducer under the hood, with a basic reducer provided.
What you've provided is essentially a reducer. The example you give can be replaced with useReducer, which is closely equivalent to what useState is doing:
const myModule = {
increment: (state) => {
return {count: state.count + 1};
},
decrement: (state) => {
return {count: state.count - 1};
}
};
const reducer = (state, action) => {
return action(state);
} ;
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch(myModule.increment)}>Increment</button>
<button onClick={() => dispatch(myModule.decrement)}>Decrement</button>
</div>
);
};
But both this and the useState example are annoying because now you'll need to pass both the dispatch and the myModule functions around to children. You can solve this by wrapping the dispatches:
const myModule = {
increment: (state) => {
return {count: state.count + 1};
},
decrement: (state) => {
return {count: state.count - 1};
}
};
const reducer = (state, action) => {
return action(state);
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatcher = useMemo(
() => ({
increment: () => dispatch(myModule.increment),
decrement: () => dispatch(myModule.decrement),
}),
[]
);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatcher.increment()}>Increment</button>
<button onClick={() => dispatcher.decrement()}>Decrement</button>
</div>
);
};
But that's a lot of indirection to follow. It's less indirection to write as typical reducer:
const reducer = (state, action) => {
switch (action) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw Error('Unknown action: ' + action.type);
}
};
const SCounter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch("increment")}>Increment</button>
<button onClick={() => dispatch("decrement")}>Decrement</button>
</div>
);
};
And if you want to pass more values to the action, the convention is to use type on the action:
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw Error('Unknown action: ' + action.type);
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({type: "increment"})}>Increment</button>
<button onClick={() => dispatch({type: "decrement"})}>Decrement</button>
</div>
);
};
Of course, if you want to use functions, you can still wrap them:
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw Error('Unknown action: ' + action.type);
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const {increment, decrement} = useMemo(() => {
return {
increment: (...args) => dispatch({type: "increment", ...args}),
decrement: (...args) => dispatch({type: "decrement", ...args}),
}
}, []);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => increment()}>Increment</button>
<button onClick={() => decrement()}>Decrement</button>
</div>
);
};
But notice that there's still less indirection than before. This also scales up better because the single reducer function operates as a function that accepts state, and an action and returns the same state object. The "switch" here is a benefit because you're able to see a list of all the actions that can be taken on the state, and the result of the action. For complex state objects, the function example may be difficult to follow and result in hard to see bugs, because essentially each function in your object is a separate reducer.