fini

Small and capable state machines for React

Usage no npm install needed!

<script type="module">
  import fini from 'https://cdn.skypack.dev/fini';
</script>

README

Fini logo

npm version GitHub license

Small and capable state machines for React.

Using finite state machines (FSM) is a great way to safely and effectively manage complexity in your UI components. Fini aims to lower the bar of getting started with this powerful concept, all while providing the important features you should expect from an FSM library:

  • ✅ easy-to-use hook for defining states, transitions and effects
  • ✅ type safety all the way
  • ✅ simple state-matching and event-dispatching from your components

Furthermore, you might like Fini if you

  • want something slightly more structured than the regular reducer
  • enjoy typing 😉
  • want to get into the basics of state machines

Sounds great, doesn't it? Head over to the documentation site, or check out the quick-ish start example.

❓ Unfamiliar with state machines? Watch this great talk by David Khourshid!


Quick-ish start

npm install fini

Simple counter example (Codesandbox)

import React from "react";
import { useMachine } from "fini";

// Define a typed schema for the machine
type CounterMachine = {
  states: {
    // Idle state which handles the `start` event
    idle: {
      events: {
        start: void;
      };
    };
    // Counting state which handles the `increment` and `set` events
    counting: {
      events: {
        increment: void;
        // the `set` event comes with a number payload
        set: number;
      };
      // Contextual data that is specific to,
      // and only available in, the `counting` state
      context: { count: number };
    };
  };
  // Context that is common for all states
  context: { maxCount: number };
  // Events that may or may not be handled by all states
  events: {
    log: void;
  };
};

const App = () => {
  const machine = useMachine<CounterMachine>(
    {
      // Object that describes the `idle` state and its supported events
      idle: {
        // Event handler function which transitions into
        // the `counting` state, and sets the current count to 0
        start: ({ update }) => update.counting({ count: 0 }),
        log: ({ update }) => {
          // execute a side-effect
          update(() => console.log("Haven't started counting yet"));
        },
      },
      counting: {
        // Updates the context by incrementing the current count,
        // if max count hasn't already been reached
        increment: ({ update, context }) =>
          update.counting({
            count:
              context.count === context.maxCount
                ? context.count
                : context.count + 1,
          }),
        set: ({ next }, count) =>
          // Update context and run a side-effect
          update.counting(
            {
              count,
            },
            () => console.log(`Count was set to ${count}`)
          ),
        log: ({ update, context }) => {
          update(() => console.log(`Current count is ${context.count}`));
        },
      },
    },
    // Set the initial state and context
    (initial) => initial.idle({ maxCount: 120 })
  );

  return (
    <div>
      {
        // Use the returned `machine` object to match states,
        // read the context, and dispatch events
        machine.idle && <button onClick={machine.start}>Start counting!</button>
      }
      {machine.counting && (
        <div>
          <p>{`Count: ${machine.context.count}`}</p>
          <button onClick={machine.increment}>Increment</button>
          <button onClick={() => machine.set(100)}>Set to 100</button>
        </div>
      )}
      <button onClick={machine.log}>Log the count</button>
    </div>
  );
};