Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/rkrupinski/use-state-machine
A simple yet powerful finite state machine React hook.
https://github.com/rkrupinski/use-state-machine
hook react state-machine state-management typescript
Last synced: about 1 month ago
JSON representation
A simple yet powerful finite state machine React hook.
- Host: GitHub
- URL: https://github.com/rkrupinski/use-state-machine
- Owner: rkrupinski
- License: mit
- Created: 2021-11-10T18:04:55.000Z (about 3 years ago)
- Default Branch: master
- Last Pushed: 2023-01-19T14:28:41.000Z (almost 2 years ago)
- Last Synced: 2024-10-28T17:39:11.964Z (about 1 month ago)
- Topics: hook, react, state-machine, state-management, typescript
- Language: TypeScript
- Homepage: https://github.com/rkrupinski/use-state-machine#readme
- Size: 201 KB
- Stars: 17
- Watchers: 3
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-react-hooks - `@rkrupinski/use-state-machine`
- awesome-react-hooks-cn - `@rkrupinski/use-state-machine`
- awesome-react-hooks - `@rkrupinski/use-state-machine`
- awesome-react-hooks - `@rkrupinski/use-state-machine`
README
# @rkrupinski/use-state-machine
A simple yet powerful finite state machine [React](https://reactjs.org/) hook.
![Build status](https://github.com/rkrupinski/use-state-machine/workflows/CI/badge.svg)
[![minified + gzip](https://badgen.net/bundlephobia/minzip/@rkrupinski/use-state-machine)](https://bundlephobia.com/package/@rkrupinski/use-state-machine)```ts
const [state, send] = useStateMachine({
initial: "enabled",
states: {
enabled: {
on: { TOGGLE: "disabled" },
},
disabled: {
on: { TOGGLE: "enabled" },
},
},
});
```
Comes packed with features like:
- effects (state entry/exit)
- guards (allow/prevent state transitions)
- extended state (context)
- good to very good TypeScript experience (see [History](#history))
Table of contents:
- [History](#history)
- [Installation](#installation)
- [Examples](#examples)
- [API](#api)
- [State](#state)
- [Events](#events)
- [Machine options](#machine-options)
- [Configuring states](#configuring-states)
- [Effects](#effects)
- [Configuring state transitions](#configuring-state-transitions)
- [Guards](#guards)
- [Event payload](#event-payload)
- [Context](#context)
- [Further reading](#further-reading)## History
This project was born as an attempt to reimplement [@cassiozen/usestatemachine](https://github.com/cassiozen/useStateMachine) in a more "friendly" way. Despite only weighing <1kB, I found the reference project being slightly overly complex, especially on the type system side of things.
ℹ️ Note: This is based on version [1.0.0-beta.4](https://github.com/cassiozen/useStateMachine/releases/tag/1.0.0-beta.4) ([source code](https://github.com/cassiozen/useStateMachine/tree/ced39beb8a119a1acb264d62f522cfa419f9e85b))
Differences compared to the reference project:
- simpler implementation
- simpler types (with added benefit of making invalid/orphan states impossible)
- manual payload typing/decoding (in place of "[schema](https://github.com/cassiozen/useStateMachine/tree/ced39beb8a119a1acb264d62f522cfa419f9e85b#schema-context--event-typing)"; see [Event payload](#event-payload) for details)
- manual context typing (in place of "[schema](https://github.com/cassiozen/useStateMachine/tree/ced39beb8a119a1acb264d62f522cfa419f9e85b#schema-context--event-typing)"; see [Context](#context) for details)## Installation
```
npm install @rkrupinski/use-state-machine
```## Examples
View [source code](packages/examples) or [live](https://use-state-machine.netlify.app).
Examples cover:
- a basic machine with context and guards
- sending events with payload
- http with error recovery## API
### State
```ts
const [
state, // <--- this guy
send,
] = useStateMachine(/* ... */);
````state` is an object of the following shape:
Name
Type
Description
value
string
The name of the current state.
nextEvents
string[]
The names of possible events.
(see Events)
event
Event
The event that led to the current state.
(see Events)
context
C
(inferred)
Machine's extended state. Think of it as a place to store additional, machine-related data throughout its whole lifecycle.
(see Context)
### Events
```ts
const [
state,
send, // <--- this guy
] = useStateMachine(/* ... */);
```Once initialized, events can be sent to the machine using the `send` function.
Name
Type
Description
send
(event: string | Event) => void
Sends events to the machine
When sending events you can either use a shorthand (`string`) syntax:
```ts
send("START");
```or the object (`Event`) syntax:
```ts
send({ type: "START" });
```Under the hood, all sent events are normalized to objects (`Event`).
ℹ️ Note: The reason behind having 2 formats is that events, apart from being of certain `type`, can also carry `payload`.
(see [Event payload](#event-payload))
### Machine options
```ts
const [state, send] = useStateMachine({
initial: "idle",
states: {
/* ... */
},
context: 42,
});
```Machine can be configured with the following options:
Name
Type
Description
initial
(required)
string
The initial machine state value.
ℹ️ Note: Must be a key ofstates
states
(required)
{ [key: string]: StateConfig }
An object with configuration for all the states.
(see Configuring states)
context
C
(inferred)
Initial context value.
(see Context)
### Configuring states
You can configure individual states using the `states` field of the machine options.
```ts
const [state, send] = useStateMachine({
/* ... */
states: {
idle: {
on: {
START: "running",
},
effect() {
console.log("idling");
},
},
/* ... */
},
});
```Keys of the `states` object are state names, values are `StateConfig` object of the following shape:
Name
Type
Description
on
{ [key: string]: string | EvtConfig }
An object with configuration for all the transitions supported by this particular state.
(see Configuring state transitions)
effect
Effect
A callback fired once the machine has transitioned to a particular state.
(see Effects)
ℹ️ Note: There can't be a state that's neither initial, nor can be transitioned to.
### Effects
You can define a callback to fire once the machine has transitioned to a particular state using the `effect` field.
```ts
const [state, send] = useStateMachine({
/* ... */
states: {
idle: {
effect({ context, setContext, event, send }) {
console.log("idling due to", event.type);return () => {
console.log("idling no more");
};
},
},
/* ... */
},
});
```The `effect` callback will receive an object of the following shape:
Name
Type
Description
context
C
(inferred)
The current value of the machine context.
(see Context)
setContext
(updater: (context: C) => C) => void
A function to update the value ofcontext
.
(see Context)
event
Event
The event that triggered the current machine state.
(see Events)
send
(event: string | Event) => void
A function to send events to the machine.
(see Events)
If the return value from `effect` is of type `function`, that function will be executed when the machine transitions away from the current state (exit/cleanup effect):
```ts
effect() {
console.log('entered a state');return () => {
console.log('exited a state');
};
},
```ℹ️ Note: Events are processed synchronously while effects are asynchronous. In other words, if several events are sent synchronously, e.g.:
```ts
send("ONE");
send("TWO");
send("THREE");
```state transitions will be performed accordingly, yet only the effect for state triggered by `THREE` (if defined) will be executed.
### Configuring state transitions
For every state you can configure when and if a transition to a different state should be performed. This is done via the `on` property of `StateConfig`.
```ts
const [state, send] = useStateMachine({
/* ... */
states: {
idle: {
on: {
START: "running",
FUEL_CHECK: {
target: "off",
guard() {
return isOutOfFuel();
},
},
},
},
off: {},
},
});
```Transition config can either be a `string` (denoting the target state value) or an object of the following shape:
Name
Type
Description
target
(required)
string
Target state value.
ℹ️ Note: Must be a key ofstates
.
(see Configuring states)
guard
Guard
Aboolean
-returning function to determine whether state transition is allowed.
(see Guards)
### Guards
The purpose of guards is to determine whether state transition is allowed. A `guard` function is invoked before performing state transition and depending on its return value:
- `true` ➡️ transition is performed
- `false` ➡️ transition is preventedA `guard` function will receive an object of the following shape:
Name
Type
Description
event
Event
The event that triggered state transition.
(see Events)
context
C
(inferred)
The current value of the machine context.
(see Context)
## Event payload
When using the object (`Event`) syntax, you can send events with payload like so:
```ts
send({
type: "REFUEL",
payload: { gallons: 5 },
});
```The payload can be then consumed from:
- the `state` object (see [State](#state))
- `effect` functions (see [Effects](#effects))
- `guard` functions (see [Guards](#guards))How is it typed though? Is the type of `payload` inferred correctly?
For several reasons, the most important of which is simplicity (see [History](#history)), this library does neither aim at inferring, nor allows providing detailed event types. Instead, it encourages using other techniques, like:
- Duck typing
- Type guards
- DecodersThe payload (`event.payload`) is always typed as `unknown` and it's up to the consumer to extract all the required information from it.
Here's an example of a `guard` function that only allows refueling if the number of gallons is at least `5`, using [io-ts](https://github.com/gcanti/io-ts) to decode the `payload`:
```ts
import * as t from "io-ts";
import { pipe } from 'fp-ts/function';
import { fold } from 'fp-ts/Either';const RefuelPayload = t.type({
gallons: t.number,
});/* ... */
guard({ event }) {
const gallons = pipe(
RefuelPayload.decode(event.payload),
fold(
() => 0,
p => p.gallons,
),
);return gallons >= 5;
}
```## Context
As mentioned above, the type of `context` is inferred from the initial value (see [Machine options](#machine-options)).
Type inference is straightforward for basic types like:
- `42` ➡️ `number`
- `'context'` ➡️ `string`
- `[1, 2, 3]` ➡️ `number[]`It gets tricky though if you need more complex constructs like:
- type narrowing (`'foo'` vs `string`)
- optionality (`{ foo?: string }`)
- unions (`'foo' | 'bar'`)Again, complex inference and annotating all the things through generic parameters is beyond the scope of this library (see [History](#history)). What it encourages instead is "hinting" TypeScript on the actual type of `context`.
This can be done via type assertions:
```ts
type ContextType = "foo" | "bar";const [state, send] = useStateMachine({
/* ... */
context: "foo" as ContextType,
});state.context; // 'foo' | 'bar'
```## Further reading
- [State machines](https://en.wikipedia.org/wiki/Finite-state_machine) on Wikipedia
- [@cassiozen/usestatemachine](https://github.com/cassiozen/useStateMachine)
- [XState](https://xstate.js.org/)