Convert Figma logo to code with AI

cassiozen logouseStateMachine

The <1 kb state machine hook for React

2,382
47
2,382
13

Top Related Projects

27,295

Actor-based state management & orchestration for complex app logic.

A javascript finite state machine library

Quick Overview

useStateMachine is a React hook that implements a finite state machine for managing complex state logic in React applications. It provides a declarative way to define states, transitions, and actions, making it easier to handle complex UI interactions and application flows.

Pros

  • Simplifies complex state management in React applications
  • Provides a clear and declarative way to define state transitions
  • Helps prevent impossible states and improves predictability
  • Integrates well with existing React ecosystems and patterns

Cons

  • Introduces a new concept (state machines) that may have a learning curve for some developers
  • May be overkill for simple state management scenarios
  • Limited community support compared to more established state management solutions
  • Requires careful planning of states and transitions for optimal use

Code Examples

  1. Basic usage of useStateMachine:
const { state, send } = useStateMachine({
  initial: 'idle',
  states: {
    idle: {
      on: { FETCH: 'loading' }
    },
    loading: {
      on: { SUCCESS: 'success', ERROR: 'error' }
    },
    success: {},
    error: {}
  }
});
  1. Using actions with state transitions:
const { state, send } = useStateMachine({
  initial: 'closed',
  states: {
    closed: {
      on: { 
        OPEN: {
          target: 'opened',
          action: () => console.log('Opening drawer')
        }
      }
    },
    opened: {
      on: { CLOSE: 'closed' }
    }
  }
});
  1. Conditional transitions:
const { state, send } = useStateMachine({
  initial: 'idle',
  states: {
    idle: {
      on: { 
        SUBMIT: [
          { target: 'loading', cond: (ctx) => ctx.isValid },
          { target: 'error' }
        ]
      }
    },
    loading: {},
    error: {}
  }
});

Getting Started

To use useStateMachine in your React project:

  1. Install the package:

    npm install @cassiozen/usestatemachine
    
  2. Import and use the hook in your component:

    import { useStateMachine } from '@cassiozen/usestatemachine';
    
    function MyComponent() {
      const { state, send } = useStateMachine({
        initial: 'idle',
        states: {
          idle: {
            on: { START: 'running' }
          },
          running: {
            on: { STOP: 'idle' }
          }
        }
      });
    
      return (
        <div>
          Current state: {state}
          <button onClick={() => send('START')}>Start</button>
          <button onClick={() => send('STOP')}>Stop</button>
        </div>
      );
    }
    

Competitor Comparisons

27,295

Actor-based state management & orchestration for complex app logic.

Pros of XState

  • More comprehensive and feature-rich, supporting complex state machines and statecharts
  • Provides visual editing and debugging tools through the XState Visualizer
  • Offers better TypeScript support and type inference

Cons of XState

  • Steeper learning curve due to its more extensive API and concepts
  • Potentially overkill for simple state management scenarios
  • Larger bundle size compared to useStateMachine

Code Comparison

XState:

import { createMachine, interpret } from 'xstate';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});

useStateMachine:

import { useStateMachine } from '@cassiozen/usestatemachine';

const [state, send] = useStateMachine({
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});

XState offers a more robust solution for complex state management scenarios, with advanced features and tooling. However, useStateMachine provides a simpler, more lightweight alternative that may be sufficient for many React applications. The choice between the two depends on the specific requirements of your project and the complexity of the state management needs.

A javascript finite state machine library

Pros of javascript-state-machine

  • More mature and widely adopted project with a larger community
  • Supports both synchronous and asynchronous transitions
  • Provides visualization tools for state diagrams

Cons of javascript-state-machine

  • Not specifically designed for React, requiring additional setup for integration
  • Heavier and more complex API compared to useStateMachine
  • Less focus on performance optimization for React applications

Code Comparison

useStateMachine:

const [state, send] = useStateMachine({
  initial: 'idle',
  states: {
    idle: { on: { FETCH: 'loading' } },
    loading: { on: { RESOLVE: 'success', REJECT: 'error' } },
    success: {},
    error: {}
  }
});

javascript-state-machine:

const fsm = new StateMachine({
  init: 'idle',
  transitions: [
    { name: 'fetch', from: 'idle', to: 'loading' },
    { name: 'resolve', from: 'loading', to: 'success' },
    { name: 'reject', from: 'loading', to: 'error' }
  ]
});

The useStateMachine library provides a more concise and React-friendly API, while javascript-state-machine offers a more traditional object-oriented approach with explicit transition definitions. useStateMachine is tailored for React applications, whereas javascript-state-machine is a more general-purpose state machine library that can be used in various JavaScript environments.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

The <1 kb state machine hook for React:

See the user-facing docs at: usestatemachine.js.org

  • Batteries Included: Despite the tiny size, useStateMachine is feature complete (Entry/exit callbacks, Guarded transitions & Extended State - Context)
  • Amazing TypeScript experience: Focus on automatic type inference (auto completion for both TypeScript & JavaScript users without having to manually define the typings) while giving you the option to specify and augment the types for context & events.
  • Made for React: useStateMachine follow idiomatic React patterns you and your team are already familiar with. (The library itself is actually a thin wrapper around React's useReducer & useEffect.)
size badge

Examples

Installation

npm install @cassiozen/usestatemachine

Sample Usage

import useStateMachine from "@cassiozen/usestatemachine";

const [state, send] = useStateMachine({
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' },
    },
    active: {
      on: { TOGGLE: 'inactive' },
      effect() {
        console.log('Just entered the Active state');
        // Same cleanup pattern as `useEffect`:
        // If you return a function, it will run when exiting the state.
        return () => console.log('Just Left the Active state');
      },
    },
  },
});

console.log(state); // { value: 'inactive', nextEvents: ['TOGGLE'] }

// Refers to the TOGGLE event name for the state we are currently in.

send('TOGGLE');

// Logs: Just entered the Active state

console.log(state); // { value: 'active', nextEvents: ['TOGGLE'] }

API

useStateMachine

const [state, send] = useStateMachine(/* State Machine Definition */);

useStateMachine takes a JavaScript object as the state machine definition. It returns an array consisting of a current machine state object and a send function to trigger transitions.

state

The machine's state consists of 4 properties: value, event, nextEvents and context.

value (string): Returns the name of the current state.

event ({type: string}; Optional): The name of the last sent event that led to this state.

nextEvents (string[]): An array with the names of available events to trigger transitions from this state.

context: The state machine extended state. See "Extended State" below.

Send events

send takes an event as argument, provided in shorthand string format (e.g. "TOGGLE") or as an event object (e.g. { type: "TOGGLE" })

If the current state accepts this event, and it is allowed (see guard), it will change the state machine state and execute effects.

You can also send additional data with your event using the object notation (e.g. { type: "UPDATE" value: 10 }). Check schema for more information about strong typing the additional data.

State Machine definition

KeyRequiredDescription
verboseIf true, will log every context & state changes. Log messages will be stripped out in the production build.
schemaFor usage with TypeScript only. Optional strongly-typed context & events. More on schema below
contextContext is the machine's extended state. More on extended state below
initial*The initial state node this machine should be in
states*Define the possible finite states the state machine can be in.

Defining States

A finite state machine can be in only one of a finite number of states at any given time. As an application is interacted with, events cause it to change state.

States are defined with the state name as a key and an object with two possible keys: on (which events this state responds to) and effect (run arbitrary code when entering or exiting this state):

On (Events & transitions)

Describes which events this state responds to (and to which other state the machine should transition to when this event is sent):

states: {
  inactive: {
    on: {
      TOGGLE: 'active';
    }
  },
  active: {
    on: {
      TOGGLE: 'inactive';
    }
  },
},

The event definition can also use the extended, object syntax, which allows for more control over the transition (like adding guards):

on: {
  TOGGLE: {
    target: 'active',
  },
};

Guards

Guards are functions that run before actually making the state transition: If the guard returns false the transition will be denied.

const [state, send] = useStateMachine({
  initial: 'inactive',
  states: {
    inactive: {
      on: {
        TOGGLE: {
          target: 'active',
          guard({ context, event }) {
            // Return a boolean to allow or block the transition
          },
        },
      },
    },
    active: {
      on: { TOGGLE: 'inactive' },
    },
  },
});

The guard function receives an object with the current context and the event. The event parameter always uses the object format (e.g. { type: 'TOGGLE' }).

Effects (entry/exit callbacks)

Effects are triggered when the state machine enters a given state. If you return a function from your effect, it will be invoked when leaving that state (similarly to how useEffect works in React).

const [state, send] = useStateMachine({
  initial: 'active',
  states: {
    active: {
      on: { TOGGLE: 'inactive' },
      effect({ send, setContext, event, context }) {
        console.log('Just entered the Active state');
        return () => console.log('Just Left the Active state');
      },
    },
  },
});

The effect function receives an object as parameter with four keys:

  • send: Takes an event as argument, provided in shorthand string format (e.g. "TOGGLE") or as an event object (e.g. { type: "TOGGLE" })
  • setContext: Takes an updater function as parameter to set a new context (more on context below). Returns an object with send, so you can set the context and send an event on a single line.
  • event: The event that triggered a transition to this state. (The event parameter always uses the object format (e.g. { type: 'TOGGLE' }).).
  • context The context at the time the effect runs.

In this example, the state machine will always send the "RETRY" event when entering the error state:

const [state, send] = useStateMachine({
  initial: 'loading',
  states: {
    /* Other states here... */
    error: {
      on: {
        RETRY: 'load',
      },
      effect({ send }) {
        send('RETRY');
      },
    },
  },
});

Extended state (context)

Besides the finite number of states, the state machine can have extended state (known as context).

You can provide the initial context value in the state machine definition, then use the setContext function within your effects to change the context:

const [state, send] = useStateMachine({
  context: { toggleCount: 0 },
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' },
    },
    active: {
      on: { TOGGLE: 'inactive' },
      effect({ setContext }) {
        setContext(context => ({ toggleCount: context.toggleCount + 1 }));
      },
    },
  },
});

console.log(state); // { context: { toggleCount: 0 }, value: 'inactive', nextEvents: ['TOGGLE'] }

send('TOGGLE');

console.log(state); // { context: { toggleCount: 1 }, value: 'active', nextEvents: ['TOGGLE'] }

Schema: Context & Event Typing

TypeScript will automatically infer your context type; event types are generated automatically.

Still, there are situations where you might want explicit control over the context and event types: You can provide you own typing using the t whithin schema:

Typed Context example

const [state, send] = useStateMachine({
  schema: {
    context: t<{ toggleCount: number }>()
  },
  context: { toggleCount: 0 },
  initial: 'inactive',
  states: {
    inactive: {
      on: { TOGGLE: 'active' },
    },
    active: {
      on: { TOGGLE: 'inactive' },
      effect({ setContext }) {
        setContext(context => ({ toggleCount: context.toggleCount + 1 }));
      },
    },
  },
});

Typed Events

All events are type-infered by default, both in the string notation (send("UPDATE")) and the object notation (send({ type: "UPDATE"})).

If you want, though, you can augment an already typed event to include arbitrary data (which can be useful to provide values to be used inside effects or to update the context). Example:

const [machine, send] = useStateMachine({
  schema: {
    context: t<{ timeout?: number }>(),
    events: {
      PING: t<{ value: number }>()
    }
  },
  context: {timeout: undefined},
  initial: 'waiting',
  states: {
    waiting: {
      on: {
        PING: 'pinged'
      }
    },
    pinged: {
      effect({ setContext, event }) {
        setContext(c => ({ timeout: event?.value ?? 0 }));
      },
    }
  },
});

send({ type: 'PING', value: 150 })

Note that you don't need to declare all your events in the schema, only the ones you're adding arbitrary keys and values.

Wiki

Contributors ✨

Thanks goes to these wonderful people (emoji key):


Cassio Zen

💻 📖 ⚠️ 🤔 🐛

Devansh Jethmalani

💻 ⚠️ 🤔 🐛

Michael Schmidt

💻 ⚠️ 🤔

Joseph

💻

Jeremy Mack

📖

Ron

📖

Klaus Breyer

📖

Arthur Denner

💻 🐛 ⚠️ 🤔

This project follows the all-contributors specification. Contributions of any kind welcome!