How to use Reducer in React for better State Management: 2 effective ways for simpler design and architecture
Explore the benefits of using the useReducer hook in React for more maintainable and readable components. (3 min)
Managing a complex state in React can be tricky.
Using multiple useState hooks for related data often results in nasty and hard-to-maintain components.
By leveraging the useReducer hook for related state variables, you can simplify your code.
We can make it even simpler by abstracting the reducer details and provide a deep and simpler interface to our components.
Understanding these techniques is important.
It will help you write more maintainable and scalable React components and applications.
Use Reducers for Complex State
⛔ Avoid using multiple `useState` hooks for states when they are somehow related.
Managing related state variables with multiple useState hooks can lead to messy and hard-to-maintain code.
This approach makes it difficult to update a state which depends on multiple state variables. It also increases the potential for bugs since it’s harder to trace how the state is updating.
The more state variables you have, the more cluttered the component will be, and the less readable and maintainable.
const App = () => {
const [locationFilter, setLocationFilter] = useState("");
const [queryFilter, setQueryFilter] = useState("");
const [pageFilter, setPageFilter] = useState("");
const handleLocationChange = (location) => {
setLocationFilter(location);
};
const handleQueryChange = (query) => {
setQueryFilter(query);
};
const handlePageChange = (page) => {
setPageFilter(page);
};
return (
...
);
};
✅ Prefer using useReducer hook for states that can be grouped.
By using useReducer, you can group the related state together into a single object which will be managed by a reducer function.
This way, we centralize the state logic.
We make the code more organized, and easier to follow and understand.
This also simplifies complex state updates and reduces the potential for errors.
By having this, we enhance the maintainability and scalability of our components.
const FILTERING_ACTION_TYPES = {
selectLocation: 'SELECT_LOCATION',
selectQueryFilter: 'SELECT_QUERY_FILTER',
selectPage: 'SELECT_PAGE',
...
};
const initialState = {
...
};
const reducer = (state, action) => {
switch (action.type) {
case FILTERING_ACTION_TYPES.selectLocation: {
return {
...
}
}
...
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleLocationChange = (location) => {
dispatch({
type: FILTERING_ACTION_TYPES.selectLocation,
payload: location,
})
};
...
return (
...
);
};
Abstract Reducer Details
⛔ Avoid having a shallow hook for exposing the reducer details and functionality.
Exposing the reducer’s internal details and the dispatch function in the components can lead to tight coupling between our state management logic and our UI components.
This can make the components more complex and less reusable since they become responsible for handling action types and payloads.
It also exposes implementation details that should remain encapsulated.
We also violate three SOLID principles - SRP, DIP, and ISP.
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleLocationChange = (location) => {
dispatch({
type: FILTERING_ACTION_TYPES.selectLocation,
payload: location,
})
};
...
return (
...
);
};
✅ Prefer abstracting the reducer details with a deep custom hook.
By encapsulating the reducer logic and details within a custom hook, we hide the implementation details.
We provide a clean interface for the components and expose only what is needed to get the job done.
We separate the state logic from the UI and component.
This makes our components more clear, readable, maintainable, and focused only on the rendering logic and user interface.
Now, the SRP, DIP, and ISP are satisfied.
const useFilters = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const updateLocationFilter = (location) =>
dispatch({
type: FILTERING_ACTION_TYPES.selectLocation,
payload: location,
});
const updatePageFilter = (page) =>
dispatch({
type: FILTERING_ACTION_TYPES.selectPage,
payload: page,
});
const updateQueryFilter = (query) =>
dispatch({
type: FILTERING_ACTION_TYPES.selectQuery,
payload: query,
});
return {
filteringState: state,
updateLocationFilter,
updatePageFilter,
updateQueryFilter,
};
};
const App = () => {
const {
filteringState,
updateLocationFilter,
updatePageFilter,
updateQueryFilter
} = useFilters();
...
return (
...
);
};
⭐ Recap
⛔ Avoid using multiple useState hooks for states when they’re are somehow related.
✅ Prefer using useReducer hook for states that can be grouped.
⛔ Avoid having a shallow hook for exposing the reducer details and functionality.
✅ Prefer abstracting the reducer details with a deep custom hook.
👋 Let’s connect
You can find me on LinkedIn or Twitter.
I share daily practical tips to level up your skills and become a better engineer.
Thank you for being a great supporter, reader, and for your help in growing to 13.1K+ subscribers this week 🙏
This newsletter is funded by paid subscriptions from readers like yourself.
If you aren’t already, consider becoming a paid subscriber to receive the full experience!
Think of it as buying me a coffee twice a month, with the bonus that you also get all my templates and products for FREE.
You can also hit the like ❤️ button at the bottom to help support me or share this with a friend to get referral rewards. It helps me a lot! 🙏