npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bazariodev/fsm-react

v1.1.2

Published

React bindings for @bazariodev/fsm sources with useSyncExternalStore subscriptions and selector bail-outs.

Readme

@bazariodev/fsm-react

React hooks for @bazariodev/fsm machines. They work with Fsm, FsmHierarchy, hierarchy node handles, or any object with a stable snapshot and a subscribe(listener) method.

Install

pnpm add @bazariodev/fsm-react @bazariodev/fsm react

Supports React 18 and 19. react and @bazariodev/fsm are peer dependencies; @bazariodev/fsm provides the shared types.

Usage

import { Fsm } from '@bazariodev/fsm';
import { useFsmSelector, useFsmSnapshot } from '@bazariodev/fsm-react';

type CallState = 'idle' | 'dialing' | 'connected';
type CallEvent = { type: 'DIAL' } | { type: 'CONNECT' };

const call = new Fsm<CallState, CallEvent, { attempts: number }>({
  name: 'call',
  initial: 'idle',
  context: { attempts: 0 },
  states: { idle: {}, dialing: {}, connected: {} },
  transitions: {
    idle: {
      DIAL: { target: 'dialing', reducer: (context) => ({ attempts: context.attempts + 1 }) },
    },
    dialing: { CONNECT: { target: 'connected' } },
  },
});

function CallButton() {
  const snapshot = useFsmSnapshot(call);
  const attempts = useFsmSelector(call, (s) => s.context.attempts);

  return (
    <button type="button" onClick={() => call.send({ type: 'DIAL' })}>
      {snapshot.value} ({attempts})
    </button>
  );
}

Components re-render only after a committed transition; rejected events don't cause renders. Send events from event handlers or effects, never during render.

Hooks

function useFsmSnapshot<TSnapshot>(source: FsmSubscribable<TSnapshot>): TSnapshot;

function useFsmSelector<TSnapshot, TSelected>(
  source: FsmSubscribable<TSnapshot>,
  selector: (snapshot: TSnapshot) => TSelected,
  isEqual?: (a: TSelected, b: TSelected) => boolean,
): TSelected;

function useFsm<TMachine extends FsmSubscribable<unknown>>(
  create: () => TMachine,
  options?: {
    attach?: (machine: TMachine) => void | (() => void);
    teardown?: (machine: TMachine) => void;
  },
): Readonly<{ machine: TMachine; snapshot: SnapshotOf<TMachine> }>;
  • The hooks use React's useSyncExternalStore, so they're safe for concurrent rendering.
  • A source must return the same snapshot object until its next commit. All Bazario machines do; a custom source that builds a new object on every read breaks the hooks.
  • selector must be pure. Inline selectors are fine: changing selector or isEqual recomputes the value without resubscribing.
  • A component re-renders only when isEqual (default Object.is) reports a change in the selected value. A selector that builds a new object or array on every call needs an isEqual, such as a shallow compare, or the component re-renders on every commit.
  • Types: FsmSubscribable, EqualityFn, UseFsmOptions, SnapshotOf.

Machines owned by a component

useFsm creates a machine for one component and ties its lifetime to that component:

import { FsmEffects } from '@bazariodev/fsm-effects';
import { useFsm } from '@bazariodev/fsm-react';

// `createCallMachine` is your own factory that returns an Fsm.
function LocalCall() {
  const { machine, snapshot } = useFsm(() => createCallMachine(), {
    attach: (machine) => {
      const effects = new FsmEffects(machine, {
        effects: {
          dialing: () => {
            // start state-entry work
          },
        },
      });
      return () => effects.stop();
    },
  });

  return (
    <button type="button" onClick={() => machine.send({ type: 'DIAL' })}>
      {snapshot.value}
    </button>
  );
}
  • create runs once, on the first render. Passing new create, attach, or teardown functions on later renders doesn't recreate or reattach the machine.
  • attach runs after mount. Its returned cleanup runs on unmount.
  • teardown runs after that cleanup and ends the machine for good. Machines that own resources need it, for example teardown: (hierarchy) => hierarchy.stop() for an FsmHierarchy.
  • In development, React StrictMode may call create twice and discard one machine. Keep hooks that run during construction (the initial onEnter, onNodeSpawned) free of side effects, and acquire sockets, timers, and runners in attach.
  • With teardown, StrictMode resets the machine once in development. StrictMode runs cleanup and setup again right after mount. After teardown the old machine is dead, so useFsm creates a fresh one at its initial state. Production doesn't do this. If a machine must never reset, create it outside the component and read it with useFsmSnapshot or useFsmSelector.

Calling send

Fsm.send and FsmHierarchy.send are class methods, so a detached reference such as onClick={machine.send} throws. Call machine.send(event), or wrap it: onClick={() => machine.send(event)}. Hierarchy node handles can be detached safely. The hooks never detach your source's methods, so class-based sources work as they are.

Slow renders

Store updates always render synchronously: wrapping send in startTransition doesn't make the re-render non-blocking. To keep input responsive while a slow part of the tree catches up, defer the value you pass to it:

import { memo, useDeferredValue } from 'react';

type Message = { id: string; text: string };

// `chat` is your machine, with a `messages` array in its context.
function Conversation() {
  const messages = useFsmSelector(chat, (s) => s.context.messages);
  const deferredMessages = useDeferredValue(messages);
  return <MessageList messages={deferredMessages} />;
}

// Deferring only helps if the slow component skips renders for unchanged props.
const MessageList = memo(function MessageList({ messages }: { messages: readonly Message[] }) {
  return (
    <ul>
      {messages.map((message) => (
        <li key={message.id}>{message.text}</li>
      ))}
    </ul>
  );
});

Server rendering

The server renders the machine's current snapshot (getServerSnapshot reads source.snapshot).

  • Don't share a module-level machine between requests. Create one per request; useFsm does this, because each server render creates its own instance.
  • The first client render must show the same state as the server. A machine created from the same config does. If the server advanced its machine before rendering, pass its final state to the client and use it as the client machine's initial and context.

In apps that use React Server Components, call these hooks from a Client Component: add 'use client' to your component's module when needed. The package's own files also start with 'use client', but that marks only the package's modules; it doesn't turn the component that imports them into a Client Component.

License

MIT