DEV Community

Cover image for The State Reducer Pattern with React Hooks
Victory Asokomeh
Victory Asokomeh

Posted on

The State Reducer Pattern with React Hooks

Introduction

I like that React is loosely opinionated, you are provided with tools and given the freedom to choose the "what” and the “how” for your project.

As exciting as having autonomy while building sounds, it can quickly become an issue. If you’ve encountered several React applications built by different engineers, you will quickly find out that everyone builds using their preferred way which usually leads to inconsistency and this can be a nightmare.

Patterns provide a way to handle complex scenarios reliably while following best practices. We’ll be diving into one of my favorite React patterns.

Today we will cover the State Reducer Pattern. This pattern was made popular by Kent C .Dodds. It helps you to control how application state is updated elegantly.

We can use this pattern with the useState and useReducer hooks from React. It makes use of a reducer, which is essentially a function that takes your current state and an action which then returns a new state value based on the action you specify.

Implementation

We'll keep things simple and implement a contrived version of Chakra-UI's useDisclosure hook. This hook allows you to handle common state open, close, and toggle scenarios.

Our example consists of a simple modal that is controlled using the return values from the useDisclosure hook.

Alright, let's see some code!

First, we define our initial state which is an object with the isOpen property and equate it to false.

const initialState = { isOpen: false };

We define action types so we don't have to manually type them repeatedly and avoid typos.

const actions = {
 ON: "ON",
 OFF: "OFF",
 TOGGLE: "TOGGLE"
} as const;
Enter fullscreen mode Exit fullscreen mode

Next, we define a reducer that takes two parameters, the state and an action.

type actionTypes = { type: keyof typeof actions };

function disclosureReducer(state: typeof initialState, action: actionTypes) {
 switch (action.type) {
   case actions.ON:
     return { isOpen: true };

   case actions.OFF:
     return { isOpen: false };

   case actions.TOGGLE:
     return { isOpen: !state.isOpen };

   default:
     return state;
 }
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript, we have to define types also :)

keyof typeof actions evaluates to the union type
ON | OFF | TOGGLE which are the possible values for our actions.

typeof initialState implies that isOpen has to be a boolean.

Next, we create the useDisclosureHook custom hook

function useDisclosure() {
 const [{ isOpen }, dispatch] = React.useReducer(
   disclosureReducer,
   initialState
 );

 const onOpen = () => dispatch({ type: actions.ON });
 const onClose = () => dispatch({ type: actions.OFF });
 const onToggle = () => dispatch({ type: actions.TOGGLE });

 return { isOpen, onOpen, onClose, onToggle };
}
export default useDisclosure;
Enter fullscreen mode Exit fullscreen mode

Our custom hook makes use of a useReducer. We pass the disclosureReducer and initialState as inputs and get the state and dispatch function as outputs.

The dispatch function is used to trigger the specific action we want. We could return this dispatch function as an output from the custom hook. In this scenario, we know what actions to expect so we use action creators onOpen, onClose, and onToggle and expose them instead.

We could also allow inversion of control by giving the consumer of our custom hook the option to specify a reducer, however, this is beyond the scope of our example.

function useDisclosure({suppledReducer = disclosureReducer} = {}) {
 const [{ isOpen }, dispatch] = React.useReducer(
   suppledReducer,
   initialState
 );
 .......
}
Enter fullscreen mode Exit fullscreen mode

Usage

Our custom hook returns the isOpen value, onOpen, onToggle and  onClose methods, which can control app behavior, as shown in our example.

import Modal from "./components/Modal";
import useDisclosure from "./hooks/useDisclosure";
import "./styles.css";

export default function App() {
 const { isOpen, onOpen, onClose } = useDisclosure();
 return (
   <div className="App">
     {isOpen && (
       <Modal title="Modal title" onClose={onClose}>
         <p>
           Modal content{" "}
           <span role="img" aria-label="Smile emoji">
             😊
           </span>
         </p>
       </Modal>
     )}

     <button onClick={onOpen}>Show modal</button>
   </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Here is the final version

import React from "react";

const initialState = { isOpen: false };

const actions = {
 ON: "ON",
 OFF: "OFF",
 TOGGLE: "TOGGLE"
} as const;

type reducerType = { type: keyof typeof actions };

function disclosureReducer(state: typeof initialState, action: reducerType) {
 switch (action.type) {
   case actions.ON:
     return { isOpen: true };

   case actions.OFF:
     return { isOpen: false };

   case actions.TOGGLE:
     return { isOpen: !state.isOpen };

   default:
     return state;
 }
}

function useDisclosure() {
 const [{ isOpen }, dispatch] = React.useReducer(
   disclosureReducer,
   initialState
 );

 const onOpen = () => dispatch({ type: actions.ON });
 const onClose = () => dispatch({ type: actions.OFF });
 const onToggle = () => dispatch({ type: actions.TOGGLE });

 return { isOpen, onOpen, onClose, onToggle };
}

export default useDisclosure;
Enter fullscreen mode Exit fullscreen mode

You can find the full implementation in this codesandbox.

The state reducer pattern allows us to effectively separate state logic from our components.

It enables us to handle complex state and side effects predictably and consistently.

This pattern can come in very handy when used in the right scenario.

I hope you give it a try!

Cover Photo by Rahul Mishra on Unsplash

Top comments (0)