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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@prisel/state

v0.0.14

Published

Declarative and decentralized state machine inspired by React

Downloads

22

Readme

@prisel/state

npm
version

Declarative and decentralized state machine inspired by React.

Get Started

Install the library from npm

npm i @prisel/state

Guide

State Function

Each state in @prisel/state is defined as a function. A state function is a declarative way to define the state's property, local states, side effects, events and transitions. A state function can usually be structured in the following way:

function MyState(): StateFuncReturn {
  // Defines local state, side effects and events
  // Defines transitions
}

A very important attribute of state function is that, it is pure. Calling a state function repeatedly should not have different results. The impure part of the state (side effect, event subscription) are handled outside of the state function by the state machine.

State functions are recommended to be named using UpperCamelCase to differentiate from normal functions. They should ideally use adjetives that describe the state, for example GameStarted, UserLoggedIn, unless we have very clear name of each state, like Child, Teenager, MidAge, Elder.

Defining State Function

A simplest state is just a noop function, like below:

function Liquid() {}

State function can take a prop to initialize. A prop can be any type.

function Liquid(liquidType: string) {}

To set this state as the initial state and run the state machine, import run, and pass the state function to it.

import { run } from "@prisel/state";

run(Liquid);
// or if Liquid takes a prop
run(Liquid, "water");

Local State

Each state can have internal state. This is useful to model numeric states which are hard to convert to individual state function. For example, we can have a temperature state.

import { useLocalState, run, StateFuncReturn } from "@prisel/state";

function Liquid(): StateFuncReturn {
  const [temperature, setTemperature] = useLocalState(
    /* initial temperature */ 0
  );

  console.log(temperature); // prints 0
}

run(Liquid);

Calling setTemperature with a different temperature will cause the liquid function to be run again in the next tick.

Side Effect

A state that does no side effect is not interesting. Let's add some side effect.

Similar to React's effect hook, @prisel/state has a useSideEffect hook that can be used to perform side effect.

useSideEffect(callback, deps);

For example:

import { useSideEffect, run, StateFuncReturn } from "@prisel/state";

function Liquid(): StateFuncReturn {
  const [temperature, setTemperature] = useLocalState(0);
  useSideEffect(() => {
    // this will be run after the boiling state function is run.
    const intervalId = setInterval(() => {
      setTemperature((oldTemp) => oldTemp + 10);
    }, 100);
    return () => {
      clearInterval(intervalId);
    };
  }, []); // an empty dependencies array means side effect will be run only once when entering this state

  useSideEffect(() => {
    console.log(temperature); // will print 0, 10, 20, 30 ...
  }); // when dependencies argument is not specified, side effect will be run every time this state runs
}

run(Liquid);

Event

Event is an important concept in a state machine. Event can cause the state to change, or trigger side effect. With events, state machine can finally "move".

Events in @prisel/state are defined outside of state function.

import { newEvent } from "@prisel/state";

const [event, emitter] = newEvent("myEvent");

newEvent returns two objects, an Event and an Emitter. Event is used to subscribe to a event. Emitter is used to dispatch an event. newEvent takes a string for the event name. Event name is only for documentation and debugging purpose. If newEvent is called twice with the same event name, two different events will be created.

To define an event that expects an event data, specify the type of the event data.

const [event, emitter] = newEvent<number>("myNumEvent");

Create extended event

We can create new event that originates from an event using fitler or map.

// filters the event by event data. If false is returned, the event will not trigger.
const filteredEvent = event.filter((eventData) => true);

// transform the event data.
const transformedEvent = event.map((eventData) => "" + eventData);

Events created from filter or map shares the same Event.ref. They can be invoke using the same Emitter.

Subscribe to event

Subscribing to an event is done using useEvent hook.

const eventResult = useEvent(event);

useEvent takes an Event to subscribe to and returns an EventResult, which is a nullable wrapper for the event data. If event is triggered, eventResult will contain the event data. Otherwise eventResult will be undefined.

import { run, newState, StateFuncReturn } from "@prisel/state";

const [heat, emitHeat] = newEvent<number>("heat");

function Liquid(): StateFuncReturn {
  const heated = useEvent(heat);
  useSideEffect(() => {
    if (heated) {
      console.log(`heated up ${heated.value} degree`);
    }
  });
}

Dispatch an event

To send an event subscribers, use Emitter returned from newEvent.

const [boil, emitBoil] = newEvent<number>("boil");

function Liquid(): StateFuncReturn {
  const boiled = useEvent(boil);
  if (boiled) {
    return newState(vapor, time);
  }
}

function Vapor(timeToBoil: number) {
  console.log(`vaporized in ${timeToBoil} seconds`);
}

run(Liquid);
emitBoil.send(10);

Transition

To transition to new state, return a new state configuration from the function. A state configuration can be constructed using newState(stateFunc, props) function.

import { useSideEffect, run, newState, StateFuncReturn } from "@prisel/state";

function State1(): StateFunReturn {
  const [done, setDone] = useLocalState(false);
  useSideEffect(() => {
    console.log("state 1");
    const timeoutId = setTimeout(() => {
      setDone(true);
    }, 1000); // transition after 1 second
    return () => clearTimeout(timeoutId);
  }, []);

  if (done) {
    return newState(State2);
  }
}

function State2(): StateFuncReturn {}

run(State1);

Nested state

State transititons are useful to describe a series of actions to be performed in sequence. Within a state, we can also start nested states. Starting a nested state is no different from starting a normal state. We will call run to start a nested state. Starting a new state is a side effect, so we should call it inside onSideEffect. A nested state is consider a child state of the current state. State machine keeps track of the current state being processed. If a nested state is run inside a state's side effect or cleanup function, then it is consider a child of that state.

Cancel nested state when parent state transitions

Nested state are automatically canceled when the parent state cancels or transitions. Cancelation work in a post-order traversal fashion. This means, the cancelation logic for a child state will run first, before the cancelation logic for a parent state. Cancelation will run the cleanup function returned in useSideEffect if the side effect has been run.

To manually cancel a nested state, we can call the exit function on the Inspector returned from run. Inspector#exit will cancel the current active state originated from the state passed to run. So if a nested state transitioned to another state, we can still cancel it.

function Child() {}

function Parent() {
  useSideEffect(() => {
    const inspector = run(Child);
    const timeoutId = setTimeout(() => {
      inspector.exit();
    }, 1000); // cancel Child after 1 second

    return () => {
      clearTimeout(timeoutId);
      // if parent is canceled, we don't need to worry about canceling Child because it will be automatically canceled.
    };
  }, []);
}

Get a callback when nested state reaches an end state

We often use nested state to process a side task and want to be notified when the side task is finished. We can do so by passing a callback to nested state, and makes sure the nested state passes the callback to new states when it transitions. The convention is to pass an object with a onEnd function as prop. We can also pass an onEnd callback to endState.

function Child(props: { onEnd: () => void }) {
  const [shouldEnd, setShouldEnd] = useLocalState(false);
  useSideEffect(() => {
    setTimeout(() => setShouldEnd(true), 1000); // transition after 1 second
  });

  if (shouldEnd) {
    return endState({ onEnd: props.onEnd }); // pass the onEnd callback to end state
  }
}

function Parent() {
  const [childEnded, setChildEnded] = useLocalState(false);
  useSideEffect(() => {
    run(Child, { onEnd: () => setChildEnded(true) });
  }, []);
}

Store nested state inspector

Sometimes we want to get hold of the Inspector returned from run, we can store it either using useLocalState or useStored. useStored will not cause the state function to run again when the value changes and we can always get the latest value from current field.

function Child() {}

function Parent() {
  const inspectorRef = useStored<Inspector | null>(null);
  useSideEffect(() => {
    inspectorRef.current = run(Child);
  }, []);
}