|||

Refactor react code to use state store instead of multiple useState hooks

logo

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;
Up next Notes on Python
Latest posts Refactor react code to use state store instead of multiple useState hooks Notes on Python Threat Modelling - Using Microsoft STRIDE Model WCAG - Notes Flutter CI/CD with Azure Devops & Firebase - iOS - Part 1 Flutter CI/CD with Azure Devops & Firebase - Android - Part 2 How to samples with AWS CDK A hashicorp packer project to provision an AWS AMI with node, pm2 & mongodb Some notes on Zeebe (A scalable process orchestrator) Docker-Compose in AWS ECS with EFS volume mounts Domain Driven Design Core Principles Apple Push Notifications With Amazon SNS AWS VPC Notes Building and Deploying apps using VSTS and HockeyApp - Part 3 : Windows Phone Building and Deploying apps using VSTS and HockeyApp - Part 2 : Android Building and Deploying apps using VSTS and HockeyApp - Part 1 : iOS How I diagnosed High CPU usage using Windbg WCF service NETBIOS name resolution woes The troublesome Git-Svn Marriage GTD (Getting things done) — A simplified view Javascript Refresher Sharing common connection strings between projects A simple image carousel prototype using Asp.net webforms and SignalR Simple logging with NLog Application logger SVN Externals — Share common assembly code between solutions Simple async in .net 2.0 & Winforms Clean sources Plus Console 2 — A tabbed console window