statekit

17/10/2026 • 3 min read

A straightforward state manager. Events are just functions. Async event? Async Function. To get the data, useSelec-bleh... Call the machine's hook and read the data, proxies handle subscriptions automagically.

useSelector((s) => s.rows) struck out, above m.data.rows
import { createMachine } from "@statekit/core"

const importMachine = createMachine({
   states: ["idle", "importing", "ready", "failed"],
   data: { fileName: "", rows: 0 },
   events: {
      importFile: async (
         { draft, setState, S },
         file: File
      ) => {
         setState(S.importing)
         try {
            const text = await file.text()
            draft.fileName = file.name
            draft.rows = text.split("\n").length
            setState(S.ready)
         } catch (error) {
            setState(S.failed)
            throw error
         }
      },
   },
})

In React, the same machine gets a hook. A component rerenders on what it reads and nothing else, with no selectors to write:

import { createMachineContext } from "@statekit/react"

const [useImportMachine] =
   createMachineContext(importMachine)

const Importer = () => {
   const m = useImportMachine()

   if (m.state === m.STATE.importing)
      return <p>Importing…</p>
   if (m.state === m.STATE.failed)
      return <p>Couldn't read that file.</p>

   return (
      <>
         <input
            type="file"
            onChange={(e) =>
               m.event.importFile(e.target.files![0])
            }
         />
         <p>
            {m.data.rows} rows from {m.data.fileName}
         </p>
      </>
   )
}

What's different

  • No selector hooks. A component reads m.data.rows and rerenders when rows changes, with nothing else to write.
  • Events are functions. Sync or async, they get a draft and your arguments. An async one publishes before its first await and when it settles.
  • Isolated. The data, events and states of one thing live in one object, and that object doesn't get registered in a store.
  • Easy to test. Call the event, then read the data. Nothing has to render.
  • Easy to share. Export the machine. Whatever imports it can read it, call it or subscribe to it, in React or not.
  • Extensible. Decorations add events and selectors to any config. The TanStack Query one puts a query behind m.data.

Outside React, the same machine:

await importMachine.event.importFile(
   new File(["a,b", "\n", "c,d"], "pairs.csv")
)
expect(importMachine.data.rows).toBe(2)

dropZone.ondrop = (e) =>
   importMachine.event.importFile(e.dataTransfer!.files[0])

The packages

PackageWhat it does
@statekit/coreRuns machines: data, events, states, selectors, subscriptions.Read
@statekit/reactA hook per machine, with rerenders limited to what a component reads.Read
@statekit/decorationsConfig wrappers that add events and selectors to machines.Read
@statekit/decoration-tanstack-queryWrites a TanStack Query's data, loading state and error into a machine.Read
@statekit/eslint-pluginTwo rules for mistakes the type checker can't see.Read

@statekit/connector is the read tracking the React package is built on, for writing adapters to other view libraries.

Status

statekit is a work in progress on -dev releases, and the API has changed before.

This site uses it: the random number on the home page is a machine.

Where to start

Installation, then your first machine. If you know the core already, rerenders has a demo of what the React hook does.

#javascript, #npm, #react, #readme, #typescript, #web-development

Loading comments...