Skip to content

Writing State Machine in React with TypeScript Using useReducer

In software development, one of the common challenges is managing and visualizing different states and transitions within your application.

Imagine that you have develop UX in which users can sort by a certain field and in a certain direction. sorting-toggle

There’s a mathematical model of computation widely used in software engineering to create predictable transitions Finite State machine.

State machines are a powerful concept in software development, enabling you to manage complex states and transitions in a structured and predictable way.

In React, you can implement a simple state machine using the built-in useReducer hook.

import { useState } from "react";
import { ValueOf } from "utils/utilityTypes";
import { useStateMachine } from "./utils/useStateMachine";

type SortingState<SortBy> = {
  sortDir: ValueOf<typeof SORTING_DIRECTION>;
  sortBy?: SortBy;
};

export type UseSorting<SortBy> = {
  toggleSorting: (a: SortBy) => void;
} & SortingState<SortBy>;

export const SORTING_DIRECTION = {
  ASC: "asc",
  DESC: "desc",
  NONE: "none",
} as const;

export const init = SORTING_DIRECTION.ASC;
export const sortMachine = {
  [SORTING_DIRECTION.NONE]: {
    TOGGLE: () => SORTING_DIRECTION.ASC,
    RESET: () => init,
  },
  [SORTING_DIRECTION.ASC]: {
    TOGGLE: () => SORTING_DIRECTION.DESC,
    RESET: () => init,
  },
  [SORTING_DIRECTION.DESC]: {
    TOGGLE: () => SORTING_DIRECTION.NONE,
    RESET: () => init,
  },
} as const;

export const useSorting = <A extends string>(
  sortingState?: SortingState<A>
): UseSorting<A> => {
  const [currentSortBy, setCurrentSortingBy] = useState<A | undefined>(
    sortingState?.sortBy
  );

  const [sortDir, sendSort] = useStateMachine(
    sortingState?.sortDir || init,
    sortMachine
  );

  const handleSorting = (by: A): void => {
    setCurrentSortingBy(by);
    sendSort(by === currentSortBy ? "TOGGLE" : "RESET");
  };

  return {
    toggleSorting: handleSorting,
    sortBy: currentSortBy,
    sortDir: sortDir,
  };
};

Understanding the Code

The code provided above defines a sorting state machine using React’s useReducer hook. It manages the sorting direction and the currently sorted column. The useSorting hook provides a simple interface to toggle and reset the sorting state.

Now, let’s break down the code to understand how it works.

Sorting State

The SortingState type represents the state of the sorting machine, including the sorting direction and the currently sorted column. The sorting direction can be one of three values: ‘asc’ (ascending), ‘desc’ (descending), or ‘none’ (no sorting).

useSorting Hook

The useSorting hook creates and manages the sorting state. It takes an optional initial sorting state and returns an object with the following properties:

State Machine Definition

The sortMachine object defines the transitions between sorting states. It uses the sorting directions as keys (‘asc’, ‘desc’, ‘none’) and specifies two possible events for each state: ‘TOGGLE’ and ‘RESET.’ When you toggle sorting, the machine transitions to the next state or back to ‘asc’ if the sorting is reset.

Implementing the State Machine with useReducer

Now, let’s dive into how this state machine is implemented using the useReducer hook.

import { useReducer } from "react";

type Machine<S> = {
  [k: string]: { [k: string]: () => S };
};
type MachineState<T> = keyof T;

type MachineEvent<T> = keyof UnionToIntersection<T[keyof T]>;

type UnionToIntersection<T> = (
  T extends unknown ? (x: T) => unknown : never
) extends (x: infer R) => unknown
  ? R
  : never;

/**
 * @description A finite state machine done with React useReducer
 */
export function useStateMachine<M>(
  initialState: MachineState<M>,
  machine: M & Machine<MachineState<M>>
): [keyof M, React.Dispatch<keyof UnionToIntersection<M[keyof M]>>] {
  const reducer = (
    state: MachineState<M>,
    event: MachineEvent<M>
  ): MachineState<M> => {
    const nextState = (machine[state] as any)[event]();
    return nextState ?? state;
  };
  return useReducer(reducer, initialState);
}

The useStateMachine function is a utility function used to create a state machine with useReducer. Let’s break down its components:

The reducer function takes the current state and an event as arguments and returns the next state by invoking the appropriate event-handler function from the state machine. If no valid transition is found, it returns the current state.

Using the State Machine

To use the state machine, you can call the useSorting hook provided earlier. Here’s how you can use it in a React component:

import React from "react";
import { useSorting } from "./useSorting";

export function MyComponent() {
  const { sortBy, sortDir, toggleSorting } = useSorting();

  return (
    <div>
      <button onClick={() => toggleSorting("column1")}>
        Toggle Column 1 Sorting
      </button>
      <button onClick={() => toggleSorting("column2")}>
        Toggle Column 2 Sorting
      </button>

      <div>
        <div>Currently sorted by: {sortBy}</div>
        <div>Sorting direction: {sortDir}</div>
      </div>
    </div>
  );
}

In this example, MyComponent uses the useSorting hook to manage sorting state so that you can toggle sorting for different columns and see the current sorting state displayed in the component.

Happy Hacking 🖖