React useReducer vs React useState: When to use one over the other?
You might have just learned about React.useState
, and now you’re hearing about React.useReducer
and to use that instead of React.useState
!
At least that happen to me. So a couple questions came into mind:
- What is
React.useReducer
used for? - When do I use
React.useReducer
instead ofReact.useState
?
Let’s go over each one of these questions, and see if I can clarify these for you.
What is React useReducer used for?
Well, anytime I hear the term reducer it reminds me of the JS Array.reducer()
function.
And anything with the term reducer in React, also reminds me of Redux reducers.
React.useReducer
in fact, is like a mini Redux reducers.
const [state, dispatch] = useReducer(reducer, initialState);
React.useReducer
is a React hook function that accepts a reducer function, and a initial state.
The hook function returns and array with 2 values.
The first value is the state value, and the second value is the dispatch function to trigger an action.
You can read my previous article, “Understanding how reducers work in Redux“, to catch up on how reducers work, and how to trigger actions with dispatch()
.
The same concept applies to React.useReducer
.
Now the question is
When do you use React.useReducer instead of React.useState?
This is quite simple actually.
Use React.useReducer when
- The state value is an object or an array.
- When the logic to update state is super complex
- You need for a more predictable, and maintainable state architecture
A great example that is a good use for React.useReducer
are forms.
Form data is typically bundled up in a key-value object. It then gets JSON.stringify()
to get sent to an API endpoint.
On top of the form data, you have form status such as pending, success, error.
And those statuses may come with messages or response data.
Here’s a code example to demonstrate how React.useReducer
helps alleviate some of this complex data.
const initialState = Object.freeze({
data: {
username: "",
password: ""
},
isValid: false,
status: "FROZEN",
error: null,
response: null
});
function enterData(state, action) {
const { fieldName, value } = action.payload || {};
if (state.data.hasOwnProperty(fieldName)) {
// Update data property values
const data = {
...state.data,
[fieldName]: value
};
return {
...state,
data,
isValid: noEmptyFields(data)
};
}
return state;
}
function noEmptyFields(data) {
if ("object" !== typeof data) {
return false;
}
const fields = Object.keys(data);
for (let index = 0; index < fields.length; index++) {
// Get field name identifier
const fieldName = fields[index];
// Get field raw value
const fieldValue = data[fieldName];
if (fieldValue.length < 1) {
return false;
}
}
return true;
}
function success(state, action) {
return {
...state,
status: "READY",
error: null,
response: action.payload
};
}
function failure(state, action) {
return {
...state,
status: "ERROR",
error: action.payload,
response: null
};
}
function reducer(state, action) {
switch (action.type) {
case "form/enter-data":
return Object.assign({}, state, enterData(state, action));
case "form/update-status":
return Object.assign({}, state, { status: action.payload });
case "form/success":
return Object.assign({}, state, success(state, action));
case "form/failure":
return Object.assign({}, state, failure(state, action));
case "form/reset":
return initialState;
default:
return state;
}
}
const FooBarForm = () => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const handleChange = (e: any) =>
dispatch({
type: "form/enter-data",
payload: {
fieldName: e.target.name,
value: e.target.value
}
});
const handleSubmit = () => {
dispatch({ type: "form/update-status", payload: "VERIFYING" });
// Simulate API fetch call
setTimeout(() => {
// Comment out a dispatch statement to see a successful
// or failure simulated calls!
dispatch({
type: "form/success",
payload: {
codeStatus: 200,
redirectToDashboard: true
}
});
// dispatch({
// type: 'form/failure',
// payload: {
// codeStatus: 401,
// message: 'Incorrect credentials. Please try again!',
// }
// });
}, 2000);
};
return (
<>
{state.status === "VERIFYING" && <h2>We're checking your creds!</h2>}
{state.error && state.error.message && <h2>{state.error.message}</h2>}
{state.response && state.response.redirectToDashboard && (
<h2>You're going to get redirected in a second</h2>
)}
<label>
Username
<input name="username" onChange={handleChange} />
</label>
<br />
<label>
Password
<input name="password" onChange={handleChange} />
</label>
<br />
<button onClick={handleSubmit} disabled={!state.isValid}>
Submit
</button>
</>
);
};
Use React.useState when
- The state value is a primitive value
- Simple UI transitions
- Logic is not complicated and can stay within the component
Here’s an example where React.useState
is just enough.
const UseStateExample = () => {
const [showHiddenPassword, setShowHiddenPassword] = React.useState(false);
const handleToggle = () => setShowHiddenPassword(!showHiddenPassword);
return (
<>
<input type={showHiddenPassword ? "text" : "password"} />
<button onClick={handleToggle}>Toggle hidden password</button>
</>
);
};
I like to tweet about React and post helpful code snippets. Follow me there if you would like some too!