To create a new machine we simply import the Machine
object and call its create
method.
import { Machine } from 'stent';
const machine = Machine.create('name-of-the-machine', {
state: { name: 'idle' },
transitions: {
'idle': {
'run': 'running'
},
'running': {
'stop': 'idle'
}
}
});
{ name: 'idle'}
is the initial state. transitions
is the place where we define all the possible states of the machine (idle
and running
) with their inputs (actions run
and stop
) that they accept. Notice that stop
is not available when we are at idle
state and run
when we are at running
state. That is an essential characteristic of the state machines - our app is doing only what we allow it to do. There is no sense to call run
if we are already running
. The state machine is eliminating side effects that lead to bugs from the very beginning. The library enforces declarative approach of programming which means that by defining the possible states and actions we clearly say what's possible in our application. The user and data flows become a lot more predictable simply because we restrict ourselves of dispatching actions at the wrong time/state.
And because after the definition the machine knows what to expect it automatically creates a couple of things for us so. Based on the transitions
property Stent generates:
- Helper methods for checking if the machine is in a particular state.
idle
state producesisIdle()
method, forrunning
we haveisRunning()
. - Helper methods for dispatching actions -
run()
andstop()
. If these methods are generators you may want to use alsorun.latest()
orstop.latest()
which still accepts the action but cancels the logic of a previous call. For example if we firerun
multiple times really quick and we want to handle only the last one we should userun.latest()
instead. Go to action-handler section to learn more.
We may use spaces and/or dashes in the state or action names but the rule is that Stent transforms the string to a camel case. For example if we have fetching data
state the machine will have isFetchingData()
method, get fresh todos
action will result in getFetchTodos()
method.
So, here's an example of how to use the machine above:
if (machine.isIdle()) {
machine.run();
}
if (machine.isRunning()) {
machine.stop();
}
console.log(machine.isIdle()); // true
The created machine may accept more than a string as a handler of the action. We may pass a function which accepts two arguments. The first one is the state machine and the second one is some meta data traveling with the action (if any). For example:
const machine = Machine.create('todo-app', {
state: { name: 'idle', todos: [] },
transitions: {
'idle': {
'add todo': function (machine, todo) {
return {
name: 'idle',
todos: [...machine.state.todos, todo]
};
}
}
}
});
machine.addTodo({ title: 'Fix that damn bug' })
The state in the context of Stent is a vanilla JavaScript object literal. The only one reserved property is name
which represents the state's name. Everything else depends on our business logic. In the example above that's the todos
array.
The handler function accepts the machine with the previous state and should return a new state in a immutable fashion. Same as the Redux's reducer, whatever we return becomes the new state.
The actual todo item is passed to the addTodo
method and it comes as a second argument of the handler.
Stent also accepts a generator function as a handler. That's inspired by the redux-saga project. The generators have couple of interesting characteristics and this library uses two of them - the ability to generate multiple results (from a single function) and the ability to pause the execution. What if we need to fetch data from the server and want to handle that process with multiple states - idle
, fetching
, done
and error
. Here's how to do it with a generator as a handler:
const machine = Machine.create('todo-app', {
state: { name: 'idle', todos: [] },
transitions: {
'idle': {
'fetch todos': function * () {
yield { name: 'fetching' };
try {
const todos = yield call(getTodos, '/api/todos');
} catch (error) {
return { name: 'error', error };
}
return { name: 'done', todos };
}
}
}
});
Assuming that getTodos
is a function that accepts an endpoint as a string and returns a promise. Inside the generator we are allowed to yield
two type of things:
- A state object (which transitions the machine to that new state)
- A call of Stent's helper functions like
call
. (more about those helpers below)
Generator as an action handler is suitable for the cases where we do more then one thing and/or have async operations.