All articlesSoftware Design

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.

Petar IvanovPetar Ivanov
4 min read
On this page

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 theuseReducerhook 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.

TSX
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.

TypeScript
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.

TypeScript
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.

TypeScript
<strong>const useFilters = () => {</strong>
  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 = () => {
  <strong>const { 
    filteringState, 
    updateLocationFilter,
    updatePageFilter,
    updateQueryFilter
  } = useFilters();</strong>
  
  ...
  
  return (
    ...
  );
};

⭐ Recap

  1. ⛔ Avoid using multiple useState hooks for states when they’re are somehow related.
  2. ✅ Prefer using useReducer hook for states that can be grouped.
  3. ⛔ Avoid having a shallow hook for exposing the reducer details and functionality.
  4. ✅ Prefer abstracting the reducer details with a deep custom hook.

Related articles

Whenever you’re ready, here’s how I can help you:

  1. 1.

    The Conscious React: React architecture, design & clean code — 100+ production tips across 6 chapters, updated for React 19, plus 4 companion repos you can clone and run.

  2. 2.

    The Conscious Node: Node.js architecture, design & clean code — 157 production tips across 10 chapters, from module boundaries to the transactional outbox and zero-downtime deploys.

  3. 3.

    The JavaScript Architect Bundle: Both books + all React companion repos + CLAUDE.md rulesets + both playbooks. The complete path from developer to architect.

  4. 4.

    Free Resources: Architecture playbooks, cheat-sheets, and the JavaScript Architect Roadmap — practical guides for leveling up to senior.

The T-Shaped Dev

Join 30K+ engineers leveling up to architect

One practical tip on JavaScript, React, Node.js, and software architecture every week. No spam, unsubscribe anytime.

Petar Ivanov

Written by

Petar Ivanov

Software engineer, author, and speaker. I help JavaScript developers grow from Mid → Senior → Architect — production-grade React, Node.js, and AI systems.