Statecharts as components. No classes. Pure declarative state transitions.
TypeScript
39
776 commits
updated Oct 12, 2026
Actor-based state-management as components. → Documentation
npm install ember-statechart-component
Anywhere in your app, though, if you don't have anywhere specific in mind, the app/app.js will be just fine:
import 'ember-statechart-component';
This instructs Ember how to render and create actors from state machines.
See: https://stately.ai/docs/migration
import { createMachine } from 'xstate';
const Toggler = createMachine({
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } },
},
});
<template>
<Toggler as |toggler|>
{{toggler.statePath}}
<button {{on 'click' (fn toggler.send 'TOGGLE')}}>
Toggle
</button>
</Toggler>
</template>
import { getService } from 'ember-statechart-component';
import { setup } from 'xstate';
const AuthenticatedToggle = setup({
actions: {
notify: ({ context }) => {
getService(context, 'toasts').notify('You must be logged in');
},
},
guards: {
isAuthenticated: ({ context }) => getService(context, 'session').isAuthenticated,
},
}).createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: [
{
target: 'active',
guard: 'isAuthenticated',
},
{ actions: ['notify'] },
],
},
},
active: { on: { TOGGLE: 'inactive' } },
},
});
<template>
<AuthenticatedToggle as |toggle|>
{{toggle.statePath}}
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</AuthenticatedToggle>
</template>
<Toggle as |toggle|>
{{#if (toggle.matches 'inactive')}}
The inactive state
{{else if (toggle.matches 'active')}}
The active state
{{else}}
Unknown state
{{/if}}
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</Toggle>
@configThis argument allows you to pass a MachineOptions for actions, services, guards, etc.
Usage:
// app/components/toggle.js
import { createMachine, assign } from 'xstate';
export default createMachine({
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: {
on: {
TOGGLE: {
target: 'inactive',
actions: ['toggleIsOn']
}
}
},
},
});
<Toggle
@config={{hash
actions=(hash
toggleIsOn=@onRoomIlluminated
)
}}
as |toggle|>
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</Toggle>
@inputProviding inputs from arguments works as you expect, following docs from XState: Input
const Toggle = createMachine({
types: {
input: {} as { numCalled?: number },
},
initial: 'inactive',
context: ({ input }) => {
return {
numCalled: input.numCalled ?? 0,
};
},
states: {
inactive: {
entry: assign({
numCalled: ({ context }) => context.numCalled + 1,
}),
on: { TOGGLE: 'active' },
},
active: {
entry: assign({
numCalled: ({ context }) => context.numCalled + 1,
}),
on: { TOGGLE: 'inactive' },
},
},
});
const input = {
numCalled: 10,
};
<template>
<Toggle @input={{input}} as |toggle|>
{{toggle.statePath}}
<button type="button" {{on "click" (fn toggle.send "TOGGLE")}}>
Toggle
</button>
</Toggle>
</template>
@contextSets the initial context. The current value of the context can then be accessed via state.context.
Usage:
import { createMachine, assign } from 'xstate';
export default createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
actions: ['increaseCounter']
}
}
},
active: {
on: {
TOGGLE: {
target: 'inactive',
actions: ['increaseCounter']
}
}
},
},
}, {
actions: {
increaseCounter: assign({
counter: (context) => context.counter + 1
})
}
});
<Toggle @context=(hash counter=0) as |toggle|>
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
<p>
Toggled: {{toggle.snapshot.context.counter}} times.
</p>
</Toggle>
@snapshotThe machine will use @snapshot as the initial state.
Any changes to this argument
are not automatically propagated to the machine.
An update event (see details below) is sent instead.
An event will be sent to the machine for you, along with all named arguments used to invoke the component.
To work with this event, use the constant provided by this library:
import { UPDATE_EVENT_NAME } from 'ember-statechart-component';
const MyMachine = createMachine({
initial: 'inactive',
states: {
[UPDATE_EVENT_NAME]: { /* ... */
/* ... */
},
});
The value of this constant is just EXTERNAL_UPDATE, but the import makes it clear why it exists, as the name does need to exactly match how the ember component manager is implemented for machines.
The yielded value from an invoked state machine has some properties on it as well as the actor that allows you to "just defer to XState" for most situations.
Given this a machine and its invocation,
import { createMachine } from 'xstate';
const Authenticator = createMachine({ /* ... */ });
<template>
<Authenticator as |auth|>
what is available on `auth`?
</Authenticator>
</template>
actor - The underlying actor that XState manages, see: The Actor Docssnapshot - The most recent snapshot available from the actorvalue - alias for snapshot.value, which represents the name of the state, or an array of states, if the current state is nested.statePath - a dot-separated string representing the current valuematches - The matches functiononTransition - A way to arbitrarily run code when the machine transitions.See the Contributing guide for details.
This project is licensed under the MIT License.
Overall
Maintained
Code review
Latest release
Releases this year
142 followers · starred May 2021
61 followers · starred Apr 2021
Statecharts as components. No classes. Pure declarative state transitions.
TypeScript
39
776 commits
updated Oct 12, 2026
Actor-based state-management as components. → Documentation
npm install ember-statechart-component
Anywhere in your app, though, if you don't have anywhere specific in mind, the app/app.js will be just fine:
import 'ember-statechart-component';
This instructs Ember how to render and create actors from state machines.
See: https://stately.ai/docs/migration
import { createMachine } from 'xstate';
const Toggler = createMachine({
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } },
},
});
<template>
<Toggler as |toggler|>
{{toggler.statePath}}
<button {{on 'click' (fn toggler.send 'TOGGLE')}}>
Toggle
</button>
</Toggler>
</template>
import { getService } from 'ember-statechart-component';
import { setup } from 'xstate';
const AuthenticatedToggle = setup({
actions: {
notify: ({ context }) => {
getService(context, 'toasts').notify('You must be logged in');
},
},
guards: {
isAuthenticated: ({ context }) => getService(context, 'session').isAuthenticated,
},
}).createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: [
{
target: 'active',
guard: 'isAuthenticated',
},
{ actions: ['notify'] },
],
},
},
active: { on: { TOGGLE: 'inactive' } },
},
});
<template>
<AuthenticatedToggle as |toggle|>
{{toggle.statePath}}
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</AuthenticatedToggle>
</template>
<Toggle as |toggle|>
{{#if (toggle.matches 'inactive')}}
The inactive state
{{else if (toggle.matches 'active')}}
The active state
{{else}}
Unknown state
{{/if}}
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</Toggle>
@configThis argument allows you to pass a MachineOptions for actions, services, guards, etc.
Usage:
// app/components/toggle.js
import { createMachine, assign } from 'xstate';
export default createMachine({
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: {
on: {
TOGGLE: {
target: 'inactive',
actions: ['toggleIsOn']
}
}
},
},
});
<Toggle
@config={{hash
actions=(hash
toggleIsOn=@onRoomIlluminated
)
}}
as |toggle|>
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
</Toggle>
@inputProviding inputs from arguments works as you expect, following docs from XState: Input
const Toggle = createMachine({
types: {
input: {} as { numCalled?: number },
},
initial: 'inactive',
context: ({ input }) => {
return {
numCalled: input.numCalled ?? 0,
};
},
states: {
inactive: {
entry: assign({
numCalled: ({ context }) => context.numCalled + 1,
}),
on: { TOGGLE: 'active' },
},
active: {
entry: assign({
numCalled: ({ context }) => context.numCalled + 1,
}),
on: { TOGGLE: 'inactive' },
},
},
});
const input = {
numCalled: 10,
};
<template>
<Toggle @input={{input}} as |toggle|>
{{toggle.statePath}}
<button type="button" {{on "click" (fn toggle.send "TOGGLE")}}>
Toggle
</button>
</Toggle>
</template>
@contextSets the initial context. The current value of the context can then be accessed via state.context.
Usage:
import { createMachine, assign } from 'xstate';
export default createMachine({
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: {
target: 'active',
actions: ['increaseCounter']
}
}
},
active: {
on: {
TOGGLE: {
target: 'inactive',
actions: ['increaseCounter']
}
}
},
},
}, {
actions: {
increaseCounter: assign({
counter: (context) => context.counter + 1
})
}
});
<Toggle @context=(hash counter=0) as |toggle|>
<button {{on 'click' (fn toggle.send 'TOGGLE')}}>
Toggle
</button>
<p>
Toggled: {{toggle.snapshot.context.counter}} times.
</p>
</Toggle>
@snapshotThe machine will use @snapshot as the initial state.
Any changes to this argument
are not automatically propagated to the machine.
An update event (see details below) is sent instead.
An event will be sent to the machine for you, along with all named arguments used to invoke the component.
To work with this event, use the constant provided by this library:
import { UPDATE_EVENT_NAME } from 'ember-statechart-component';
const MyMachine = createMachine({
initial: 'inactive',
states: {
[UPDATE_EVENT_NAME]: { /* ... */
/* ... */
},
});
The value of this constant is just EXTERNAL_UPDATE, but the import makes it clear why it exists, as the name does need to exactly match how the ember component manager is implemented for machines.
The yielded value from an invoked state machine has some properties on it as well as the actor that allows you to "just defer to XState" for most situations.
Given this a machine and its invocation,
import { createMachine } from 'xstate';
const Authenticator = createMachine({ /* ... */ });
<template>
<Authenticator as |auth|>
what is available on `auth`?
</Authenticator>
</template>
actor - The underlying actor that XState manages, see: The Actor Docssnapshot - The most recent snapshot available from the actorvalue - alias for snapshot.value, which represents the name of the state, or an array of states, if the current state is nested.statePath - a dot-separated string representing the current valuematches - The matches functiononTransition - A way to arbitrarily run code when the machine transitions.See the Contributing guide for details.
This project is licensed under the MIT License.
Overall
Maintained
Code review
Latest release
Releases this year
142 followers · starred May 2021
61 followers · starred Apr 2021