Let’s say you have a component that uses 4 useState hooks to manage the state of a simple counter app.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [min, setMin] = useState(0);
const [max, setMax] = useState(10);
const increment = () => {
if (count + step <= max) {
setCount(count + step);
}
};
const decrement = () => {
if (count - step >= min) {
setCount(count - step);
}
};
const reset = () => {
setCount(0);
};
const handleStepChange = (event) => {
setStep(Number(event.target.value));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
<br />
<label>
Step:
<input type="number" value={step} onChange={handleStepChange} />
</label>
</div>
);
}
export default Counter;
This is quite tedious to manage and can get out of hand quickly as we add more state to this component. Let’s refactor this code to use a reducer function instead.
import React, { useReducer } from "react";
const initialState = {
count: 0,
step: 1,
min: 0,
max: 10,
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return state.count + state.step <= state.max ? { ...state, count: state.count + state.step } : state;
case "decrement":
return state.count - state.step >= state.min ? { ...state, count: state.count - state.step } : state;
case "reset":
return { ...state, count: 0 };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: "increment" });
};
const decrement = () => {
dispatch({ type: "decrement" });
};
const reset = () => {
dispatch({ type: "reset" });
};
const handleStepChange = (event) => {
dispatch({ type: "setStep", payload: Number(event.target.value) });
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
<br />
<label>
Step:
<input type="number" value={state.step} onChange={handleStepChange} />
</label>
</div>
);
}
export default Counter;
This is easier to maintain because all state is now managed through a single function. This is also easier to test because the reducer function is a pure function (no side-effects) that takes in state and an action and returns a new state.
Now let’s create a store to manage the state and actions using Zustand (a state management library for react/next.js).
import create from "zustand";
const initialState = {
count: 0,
step: 1,
min: 0,
max: 10,
};
const useCounterStore = create((set) => ({
...initialState,
increment: () =>
set((state) => {
if (state.count + state.step <= state.max) {
return { ...state, count: state.count + state.step };
}
return state;
}),
decrement: () =>
set((state) => {
if (state.count - state.step >= state.min) {
return { ...state, count: state.count - state.step };
}
return state;
}),
reset: () => set(() => ({ ...initialState })),
setStep: (step) => set((state) => ({ ...state, step })),
}));
export default useCounterStore;
And finally, the actual component is so much simpler as a result
import React from "react";
import useCounterStore from "./counterStore";
function Counter() {
const { count, step, increment, decrement, reset, setStep } = useCounterStore();
const handleStepChange = (event) => {
setStep(Number(event.target.value));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
<br />
<label>
Step:
<input type="number" value={step} onChange={handleStepChange} />
</label>
</div>
);
}
export default Counter;