Switch statements are typically used in useReducer
as a remnant from reducers in redux.
Your second example is a good way of using an approximation of this.setState
in a function component, since useState
is only really designed for a single value as there is no shallow merging of the old state and the new. I have extended this to one step further at the end of this answer.
As for your question of which is best to manage state in a useReducer
, it really depends on what you want to use it for and how. You aren't just limited to those two types of things: you can use anything in them. I've had good luck using redux toolkit's createSlice in a useReducer
for a type-safe reducer with Immer to make immutability easier.
I don't understand why 1 is needed: it needs a switch statement which
is complex; if one wants to add state, a new case is needed
If you write a reducer case for each part of the state, yes. It is super cumbersome and I would definitely do it a different way. The best way to use the first approach is when you have more complicated situations you need to work with or generic ways to work with more state options.
As written in the React docs:
useReducer is usually preferable to useState when you have complex
state logic that involves multiple sub-values or when the next state
depends on the previous one. useReducer also lets you optimize
performance for components that trigger deep updates because you can
pass dispatch down instead of callbacks.
They are a very powerful addition to function components and allow for an easier way to work with complex logic or values that are connected logically. Whether you use it or not is of course up to you and anything that is done with useReducer
can be done with useState
s with varying amount of boilerplate and logic.
For a generic way to work with a lot of state properties:
const { useRef, useReducer } = React;
const dataReducer = (state, action) => {
switch (action.type) {
case 'toggle':
return {
...state,
[action.name]: !state[action.name],
};
case 'change':
return {
...state,
[action.name]: action.value,
};
default:
return state;
}
};
function Example() {
const [data, dispatch] = useReducer(dataReducer, {
check1: false,
check2: false,
check3: false,
input1: '',
input2: '',
input3: '',
});
const throwErrorRef = useRef(null);
const handleChange = function (e) {
const { name, value } = e.currentTarget;
dispatch({ type: 'change', name, value });
};
const handleToggle = function (e) {
const { name } = e.currentTarget;
dispatch({ type: 'toggle', name });
};
const checkBoxes = ['check1', 'check2', 'check3'];
const inputs = ['input1', 'input2', 'input3'];
return (
<div>
{checkBoxes.map((name) => (
<label>
{name}
<input
type="checkbox"
name={name}
onChange={handleToggle}
checked={data[name]}
/>
</label>
))}
<br />
{inputs.map((name) => (
<label>
{name}
<input
type="text"
name={name}
onChange={handleChange}
value={data[name]}
/>
</label>
))}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
As for slightly more complex logic, here's an example for a data fetch:
const { useRef, useReducer } = React;
const dataReducer = (state, action) => {
switch (action.type) {
case 'fetchStart':
return {
loading: true,
data: null,
error: null,
};
case 'fetchError':
if (!state.loading) {
return state;
}
return {
loading: false,
data: null,
error: action.payload.error,
};
case 'fetchSuccess': {
if (!state.loading) {
return state;
}
return {
loading: false,
data: action.payload.data,
error: null,
};
}
default:
return state;
}
};
function Example() {
const [{ loading, data, error }, dispatch] = useReducer(dataReducer, {
loading: false,
data: null,
error: null,
});
const throwErrorRef = useRef(null);
const handleFetch = function () {
if (loading) {
return;
}
dispatch({ type: 'fetchStart' });
const timeoutId = setTimeout(() => {
dispatch({ type: 'fetchSuccess', payload: { data: { test: 'Text' } } });
}, 5000);
throwErrorRef.current = () => {
clearTimeout(timeoutId);
dispatch({ type: 'fetchError', payload: { error: 'Oh noes!' } });
};
};
const handleFetchError = function () {
throwErrorRef.current && throwErrorRef.current();
};
return (
<div>
<button onClick={handleFetch}>Start Loading</button>
<button onClick={handleFetchError}>Throw an error in the fetch!</button>
<div>loading: {`${loading}`}</div>
<div>error: {error}</div>
<div>data: {JSON.stringify(data)}</div>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
A simple one I've had use of is a force update that just increments a value to cause the component to re-render.
const [,forceUpdate] = useReducer((state)=>state+1,0);
// Example use: forceUpdate();
I modified your example 2 to add support for the function method of updating the state so it's closer to a full setState
knockoff using useReducer
. I can't think of a decent way to make the callback work (the second parameter in this.setState
)
const { useRef, useReducer } = React;
const stateReducer = (state, action) => {
if (typeof action === 'function') {
action = action(state);
}
return { ...state, ...action };
};
const useMergeState = (initialState) => {
return useReducer(stateReducer, initialState);
};
function Example() {
const [state, setState] = useMergeState({
loading: false,
data: null,
error: null,
count: 0,
});
const throwErrorRef = useRef(null);
const handleFetch = function () {
if (state.loading) {
return;
}
setState({ loading: true });
const timeoutId = setTimeout(() => {
setState({
data: { text: 'A super long text', loading: false, error: null },
});
}, 5000);
throwErrorRef.current = () => {
clearTimeout(timeoutId);
setState({ error: 'Oh noes!', loading: false, data: null });
};
};
const handleFetchError = function () {
throwErrorRef.current && throwErrorRef.current();
};
const incrementCount = function () {
setState((state) => ({ count: state.count + 1 }));
setState((state) => ({ count: state.count + 1 }));
};
return (
<div>
<button onClick={handleFetch}>Start Loading</button>
<button onClick={handleFetchError}>Throw an error in the fetch!</button>
<div>loading: {`${state.loading}`}</div>
<div>error: {state.error}</div>
<div>data: {JSON.stringify(state.data)}</div>
<button onClick={incrementCount}>increase count by 2</button>
<div>count: {state.count}</div>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"/>