The State Reducer Pattern with React Hooks
Tags: react, typescript, programming, 100daysofcode
Published: 2023-02-13 08:03:14

## 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](https://kentcdodds.com/). It helps you to control how application state is updated elegantly.

We can use this pattern with the [useState](https://reactjs.org/docs/hooks-reference.html#usestate) and [useReducer](https://reactjs.org/docs/hooks-reference.html#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](https://chakra-ui.com/docs/hooks/use-disclosure) 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;
```

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;
 }
}
```

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;
```

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
 );
 .......
}
```

## 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>
 );
}
```

## 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;
```

You can find the full implementation in this [codesandbox](https://codesandbox.io/s/state-reducer-demo-rcx6sf?file=/src/App.tsx:0-569).

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 <a href="https://unsplash.com/@rahuulmiishra?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Rahul Mishra</a> on <a href="https://unsplash.com/photos/JpF58ANavoc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>