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.
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:
toggleSorting
: A function to toggle the sorting direction for a specific column.sortBy
: The currently sorted column.sortDir
: The current sorting direction.
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:
-
Machine<S>
: This type represents the structure of the state machine, where each key corresponds to a state and maps to an object with event-handler functions. The event-handler functions return the next state. -
MachineState<T>
: This type derives the possible states of the state machine from the providedMachine
type. -
MachineEvent<T>
: This type derives the possible events from theMachine
type and creates a union of all possible events. -
UnionToIntersection<T>
: This type helper merges the event-handler functions from theMachine
type into a single union type. -
useStateMachine<M>
: This is the main function that creates the state machine. It takes an initial state and the state machine definition as arguments and returns an array with two elements. The first element is the current state, and the second element is a dispatch function to trigger state transitions.
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 🖖