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