The ½ kb state machine hook for React:
- Feature complete (Entry/exit callbacks, Guarded transitions & Extended State - Context)
- Heavy focus on type inference (you get auto completion for both TypeScript & JavaScript users without having to manually define the typings)
- Idiomatic React patterns (Since it's built on top of React's useReducer & useEffect, might as well...)
- Examples Walkthrough video: YouTube
- Complex UI (Hiding and showing UI Elements based on the state) - CodeSandbox - Source
- Async orchestration (Fetch data with limited retry) - CodeSandbox - Source
$ npm install @cassiozen/usestatemachine
const [state, send] = useStateMachine()({
initial: 'inactive',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
effect() {
console.log('Just entered the Active state');
// Same cleanup pattern as `useEffect`:
// If you return a function, it will run when exiting the state.
return () => console.log('Just Left the Active state');
},
},
},
});
console.log(state); // { value: 'inactive', nextEvents: ['TOGGLE'] }
// Refers to the TOGGLE event name for the state we are currently in.
send('TOGGLE');
// Logs: Just entered the Active state
console.log(state); // { value: 'active', nextEvents: ['TOGGLE'] }
useStateMachine is a curried function (Yummm tasty!) because TypeScript doesn't yet support partial generics type inference. This workaround allows TypeScript developers to provide a custom type for the context while still having TypeScript infer all the types used in the configuration (Like the state & transitions names, etc...).
const [state, send] = useStateMachine(/* Optional Context */)(/* Configuration */);
useStateMachine
takes a JavaScript object as context (optional, see below) and one as the state machine configuration. It returns an array consisting of a current machine state
object and a send
function to trigger transitions.
Machine state
The state
consists of three properties: value
, nextEvents
and context
.
value
returns the name of the current state. nextEvents
returns an array with the names of available transitions from this state.
Send events
send
takes a event as argument. If a transition exists for this event and is allowed (see guard), it will change the state machine state and execute effects.
The configuration object should contain:
- initial: The initial state node this machine should be in
- verbose(optional): If true, will log every context & state changes. Log messages will be stripped out in the production build.
- states: Define each of the possible states:
const [state, send] = useStateMachine()({
initial: 'inactive',
verbose: true,
states: {
inactive: {},
active: {},
},
});
A state transition defines what the next state is, given the current state and event. State transitions are defined on state nodes, in the on property:
on: {
TOGGLE: 'active';
}
// (Where TOGGLE stands for an event name that will trigger a transition.)
Or the extended, object syntax, which allows for more control over the transition (like adding guards):
on: {
TOGGLE: {
target: 'active',
},
};
Effects are triggered when the state machine enters a given state. If you return a function from your effect, it will be invoked when leaving that state (similarly to how useEffect works in React).
const [state, send] = useStateMachine()({
initial: 'active',
states: {
active: {
on: { TOGGLE: 'inactive' },
effect() {
console.log('Just entered the Active state');
return () => console.log('Just Left the Active state');
},
},
},
});
The effect function receives two params: the send
method (So you can trigger transitions from within an effect) and and updater function, to update the context (more on context below).
In this example, the state machine will always send the "RETRY" event when entering the error state:
const [state, send] = useStateMachine()({
initial: 'loading',
states: {
/* Other states here... */
error: {
on: {
RETRY: 'load',
},
effect(send) {
send('RETRY');
},
},
},
});
You can set up a guard per transition, using the transition object syntax. Guard run before actually running the transition: If the guard returns false the transition will be denied.
const [state, send] = useStateMachine()({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
guard(context) {
// Return a boolean to allow or block the transition
},
},
},
},
active: {
on: { TOGGLE: 'inactive' },
},
},
});
Besides the finite number of states, the state machine can have extended state (known as context).
You can provide the initial context value as the first argument to the State Machine hook, and use the update function within your effects to change the context:
const [state, send] = useStateMachine({ toggleCount: 0 })({
initial: 'idle',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
effect(send, update) {
update(context => ({ toggleCount: context.toggleCount + 1 }));
},
},
},
});
console.log(state); // { context: { toggleCount: 0 }, value: 'inactive', nextEvents: ['TOGGLE'] }
send('TOGGLE');
console.log(state); // { context: { toggleCount: 1 }, value: 'active', nextEvents: ['TOGGLE'] }
The context types are inferred automatically in TypeScript, but you can provide you own typing if you want to be more specific:
const [state, send] = useStateMachine<{ toggleCount: number }>({ toggleCount: 0 })({
initial: 'idle',
states: {
inactive: {
on: { TOGGLE: 'active' },
},
active: {
on: { TOGGLE: 'inactive' },
effect(send, update) {
update(context => ({ toggleCount: context.toggleCount + 1 }));
},
},
},
});
Thanks goes to these wonderful people (emoji key):
Cassio Zen 💻 📖 |
Michael Schmidt 💻 |
Joseph 💻 |
Jeremy Mack 📖 |
Ron 📖 |
Klaus Breyer 📖 |
This project follows the all-contributors specification. Contributions of any kind welcome!