I may not be fully understanding how to use this library, so forgive me if I'm way off base. I'm building a React ecommerce app and since the state was growing out of control, I thought this might be a good way to reign it in a bit. So let's say I have a couple modules in my app:
The way I initially read the docs, I would think each of those modules should be separate machines, maybe with still smaller machines inside. Based on what I saw in the docs, that would be done something like this:
const userSettingsMachine = Machine({ /* ... */ })
const productFlowMachine = Machine({ /* ... */ })
const checkoutMachine = Machine({ /* ... */ })
const appMachine = Machine({
/* ... */
states: {
/* ... */
userSettings: {
invoke: {
src: userSettingsMachine,
onDone: /* ... */
}
},
products: {
invoke: {
src: productFlowMachine,
onDone: /* ... */
}
},
checkout: {
invoke: {
src: checkoutMachine,
onDone: /* ... */
}
}
}
}
However, when I do this, I can't figure out how to access the context of the inner machines, which is kind of a deal breaker for my UI. I can do it with the simple Hierarchical example in the docs (which is what I ended up doing for now), but then my appMachine's context is bloated with the context for all the child nodes.
So, what's the proper way to go about this? Maybe I've missed something conceptually, or in the docs (or in the source code), but I read them pretty thoroughly and didn't see anything that seemed to point me in the right direction. This feels like it would be a common use case, but can't seem to find anything like it.
Thanks in advance.
I can't figure out how to access the context of the inner machines, which is kind of a deal breaker for my UI
This is conceptually the same as, for example, making a fetch request, getting the response data, and setting it to the local state.
The onDone transition will be taken due to a 'done.state.<state> event, which will contain data passed to it from the invoked machine on the data: ... property.
This currently works just for promises, and I still have to add data-passing for states:
someState: {
invoke: {
src: (ctx) => fetch(`api/users/${ctx.id}`)
.then(response => response.json()),
onDone: {
// set data to context
actions: assign({ user: (ctx, e) => e.data })
target: 'success'
}
}
}
I'll get this in 馃敎 I promise! (no pun intended)
Okay, so if I'm understanding this correctly, I don't have access to the underlying context until the machine has completed? I'm guessing I wasn't using that feature for the right purpose then. So regular hierarchical states are probably the way to go for my use case it seems.
Well, you can still use it that way, and the code you have looks fine and much cleaner than if you were to do nested hierarchical machines.
As with the Actor model, data is shared via passing messages (events). Again, using fetch() as an analogy, the invoking service (the browser) cannot read the "context" of the invoked service (the 3rd-party API), and instead gets the relevant data when the invoked service _responds_ to the invoking service (the fetch().then(...) response).
So in the near future, your machines might look like:
const userSettingsMachine = Machine({
id: 'userSettings',
initial: 'step-1',
states: {
'step-1': {
// will send { type: 'haveThisData', data: ... } to parent
onEntry: sendParent('haveThisData', { data: ctx => ctx.something })
},
// ...
'step-last': {
type: 'final',
// this is what is sent to the parent machine with
// the 'done...' event
data: ctx => ctx.user
}
}
});
Thank you so much, it all just clicked for me!
I'm going to keep this open just to track it for 4.1.
Word, also one more question: can I access the state of a child machine? Like in hierarchical, I can look at the parent state and it would say like { checkout: 'submit' } for instance. I'm asking for doing like a React UI. The example in the docs is pretty simple, so I'm wondering how to use something complex like this, like if I wanted to have a self contained checkout flow with the user making inputs and changes, that updates the parent context when it's done.
can I access the state of a child machine?
That would also have to be "reported" from the child machine, via sendParent('UPDATE_STATUS', { data: ... }), especially since it changes over time, so it makes sense for it to be event-based.
An analogy: it's like ordering at a restaurant. You cannot know the status of your food (a separate kitchen service) unless the server comes and tells you the status. Of course, you know when it's done because you'll receive it.
I'd also avoid reporting the _exact_ state value to the parent, which is an implementation detail. Instead, consider an interface like PENDING | COOKING | DONE, even though the actual implementation might have more states, e.g., PENDING | PREPARING_INGREDIENTS | COOKING_MAIN_DISH | COOKING_SIDE_DISH | DONE. So you'd send data to the parent based on that common interface, rather than the implementation details.
gotcha, thank you
Hi @davidkpiano. I'd like to +1 the ability to read child context. I'm also working on a checkout flow with a substantial amount of state, spread across 3 checkout steps.
The parent state machine - which we'll call checkout - has 3 main states:
loading (initial) - fetches data needed to initialize checkouterrorreadyAs soon as the checkout state machine reaches ready, it will enter a transient state to determine which step it should initialize in. By default this is step-1. This is where I invoke the stepOneMachine using the src key and setting it equal to a Machine imported from another file, like so:
import { Machine } from 'xstate'
import { assign } from 'xstate/lib/actions'
import stepOneMachine from '../machines/stepOne'
import { initSignup } from '../api/cas/actions'
export default Machine(
{
id: 'checkout',
context: {
mls: {},
selectedTerritory: null,
selectedMls: null,
products: {},
},
initial: 'loading',
states: {
loading: {
invoke: {
src: 'init',
onDone: {
actions: ['handleInitSuccess'],
target: 'ready',
},
},
on: {
INIT_SUCCESS: 'ready',
INIT_FAILURE: 'error',
},
},
error: { INIT: 'loading' },
ready: {
initial: 'unknown',
states: {
unknown: {
on: {
'': [
{ target: 'stepOne' }, // need to check localStorage and url params to decide which step we begin on, which is `mls` for now
],
},
},
stepOne: {
invoke: {
src: stepOneMachine
},
on: {
SELECT_TERRITORY: {
actions: assign({
selectedTerritory: (ctx, event) => event.data,
}),
},
SELECT_MLS: {
actions: assign({ selectedMls: (ctx, event) => event.data }),
},
},
},
},
},
},
},
{
services: {
init: () => initSignup(),
},
actions: {
handleInitSuccess: assign({
mls: (ctx, event) => event.data.mls,
products: (ctx, event) => event.data.products,
}),
},
}
)
Here's the child state machine (stepOneMachine) we're invoking:
export default Machine(
{
id: 'stepOne',
context: {
territoryInput: '',
mlsInput: '',
},
initial: 'incomplete',
states: {
incomplete: {
on: {
SELECT_MLS: { target: 'ready', actions: 'setSelectedMls' },
VIEW_NON_MEMBER_PLANS: {
actions: sendParent('VIEW_NON_MEMBER_PLANS'),
},
},
},
ready: {},
complete: {
on: { EDIT: 'history' },
},
history: { type: 'history' },
},
},
{
actions: {
setSelectedMls: () => {}, // TODO: needs to pass result up to PlanPicker context
},
}
)
I'm using a simple React Hook to interpret these machines:
import { useState, useMemo, useEffect } from 'react'
import { interpret } from 'xstate/lib/interpreter'
export default function useStateMachine(machine, options = {}) {
// Keep track of the current machine state
const [current, setCurrent] = useState(machine.initialState)
// Start the service (only once!)
const service = useMemo(
() =>
interpret(machine)
.onTransition(state => {
// Update the current machine state when a transition occurs
options.log && console.log('CONTEXT:', state.context)
setCurrent(state)
})
.onEvent(e => options.log && console.log('EVENT:', e))
.start(),
[]
)
// Stop the service when the component unmounts
useEffect(() => {
return () => service.stop()
}, [])
return [current, service.send]
}
Within my React Components, I'm interpreting the checkout state machine and passing its state and service.send through a React.createContext() so that children can access these value however deep down the component tree they are:
export default function Checkout() {
const [state, send] = useStateMachine(machine, { log: true })
return (
<CheckoutProvider value={[state, send]}>
{state.matches('loading') && <LoadingPlanPicker />}
{state.matches('error') && <ErrorPlanPicker />}
{state.matches('ready') && (
<Container>
<StepOne />
<StepTwo />
<StepThree />
</Container>
)}
</CheckoutProvider>
)
}
So naturally, within StepOne, I will read this Checkout context value:
export default function StepOne() {
const [state, send] = useContext(CheckoutContext)
const isCurrentStep = state.matches('ready.stepOne')
return (
<StepWrapper {...{ isCurrentStep }}>
<StepHeader>
<StepIdentifier>
<StepNumber {...{ isCurrentStep }}>1</StepNumber>
<StepName {...{ isCurrentStep }}>MLS</StepName>
</StepIdentifier>
</StepHeader>
<ul>
{state.context.mls.territories.map(t =>
<li key={t} onClick={() => send('SELECT_TERRITORY')}>{t}</li>
}
</ul>
</StepWrapper>
)
}
The value of state at this point, which would be the state of the parent Checkout Machine, contains no data about the state of invoked machines.
I have also tried an approach where I'm reading the stepOneMachine with useStateMachine in the StepOneComponent, and invoking this service by id within the checkout state machine.
// ... in checkout state machine
{ ...
invoke: 'stepOne'
}
// ... in MlsStep.js react component
const [state,send] = useStateMachine(stepOneMachine)
When doing this, I get an error like this from the xstate interpreter:
No service found for invocation '[object Object]' in machine 'checkout'.
I hope I've stated my intended use case clearly through this example. I'm aiming to have each step of a checkout process encapsulate its state within its own state machine. Its totally possible that I'm thinking about this all wrong, but some state needs to be shared across all three steps, and is therefore stored by the checkout state machine's context and passed from the top of the component tree down using React.Context. Other extended state just belongs in the child states, and I'd like to be able to have each slice of UI call on useStateMachine to read and update the state that it cares about, occasionally firing off a sendParent when needed to update state that the rest of the app might care about.
I really love XState and want to be able to use it in my projects, but can't see a way to nail down this use case without having one giant state machine at the top. Thanks for all the time you put in here, I'm open to suggestions.
My suggestion (and what I currently do): the dilemma here is that you want:
Consider an actual real-life checkout flow. The credit card machine and the register are (usually) two completely separate machines that communicate with each other. The only way you can "read state" from the credit card machine would be to request its state and wait for a response (much like any REST API).
There's a couple simpler solutions to this. First, if you want to completely own the logic, you could include the machine instead of invoking it:
stepOne: {
...stepOneMachine
on: {
// ...
}
}
You'd have to refactor context though.
Or you can relinquish control of that machine to React itself:
stepOne: {
// no invoke or nested machine
on: {
// ...
}
}
And have that stepOneMachine belong to a <StepOne /> component, which sends events to the parent, similar to the TodoMVC example: https://xstate.js.org/docs/examples/todomvc.html
// Send events to parent
<StepOne onEvent={e => this.send(e)} />
You'd replace sendParent with another action that calls that onEvent() prop.
Overall, the best way to conceptualize why invoking a service doesn't give you immediate full transparency to that service's state is by thinking of it like a REST API. The only way you can see some "state" of the API is by making a GET request, and waiting for a response.
I'm currently working through this for a project. It's actually a POS system, so checkout is one of the things it has to do. I don't have that part finished, but I can show you how I do authentication, which uses the same nesting machines concept.
Auth.ts
// ...
export enum AuthStatus {
CheckAuth = "Auth/CheckAuth",
Login = "Auth/Login",
Submitting = "Auth/Submitting",
Fail = "Auth/Fail"
}
export const Chart: MachineConfig<AuthContext, AuthSchema, AuthEvent> = {
id: "auth",
context: {
email: "",
password: "",
passcode: "",
error: null
},
initial: "checkAuth",
states: {
checkAuth: {
onEntry: "statusCheckAuth", // this sends a status to the parent machine, check the actions object
invoke: {
src: "checkAuth", // just a regular fetch request that returns a promise
onDone: "done",
onError: "enterInfo"
}
},
enterInfo: {
onEntry: sendStatus(AuthStatus.Login),
on: {
[AuthEventType.EnterEmail]: {
target: "enterInfo",
actions: ["updateEmail", "statusLogin"]
},
[AuthEventType.EnterPassword]: {
target: "enterInfo",
actions: ["updatePassword", "statusLogin"]
},
[AuthEventType.EnterPasscode]: {
target: "enterInfo",
actions: ["updatePasscode"]
},
[AuthEventType.Submit]: {
target: "submitting",
cond: "canSubmit"
}
}
},
submitting: {
onEntry: "statusSubmitting",
invoke: {
src: "login",
onDone: "done",
onError: "fail"
}
},
fail: {
onEntry: ["updateError", "statusFail"],
on: {
[AuthEventType.TryAgain]: {
target: "enterInfo"
}
}
},
done: {
type: "final",
data: {
user: (ctx, evt) => evt.data.user,
posUser: (ctx, evt) => evt.data.posUser,
posProfile: (ctx, evt) => evt.data.posProfile
}
}
}
};
export const actions: ActionFunctionMap<AuthContext> = {
statusCheckAuth: sendStatus(AuthStatus.CheckAuth),
statusLogin: sendStatus(AuthStatus.Login, (ctx, evt) => ({
...ctx,
...R.omit(["type"], evt)
})),
statusSubmitting: sendStatus(AuthStatus.Submitting, ctx => ctx),
statusFail: sendStatus(AuthStatus.Fail, (ctx, evt) => ({ error: evt.data })),
updateEmail: assign<AuthContext, EnterEmailEvent>({
email: (ctx, evt) => evt.email
}),
updatePassword: assign<AuthContext, EnterPasswordEvent>({
password: (ctx, evt) => evt.password
}),
updatePasscode: assign<AuthContext, EnterPasscodeEvent>({
passcode: (ctx, evt) => evt.passcode
}),
updateError: assign<AuthContext, SubmitFailEvent>({
error: (ctx, evt) => evt.data
})
};
export const guards: Record<
string,
ConditionPredicate<AuthContext, AuthEvent>
> = {
canSubmit: ctx => !!((ctx.email && ctx.password) || ctx.passcode)
};
export const services: Record<string, ServiceConfig<AuthContext>> = {
checkAuth: () => checkAuth(),
login: ctx => login(ctx.email, ctx.password)
};
export default Machine(Chart, { actions, guards, services });
That's not the whole thing, but the rest of it is just type definitions. sendStatus is just a wrapper around sendParent just to ensure a status in my app always implements a common interface, so we don't get confused with too many different implementations of a status. That said, it might be overkill for your needs. I'll include the code anyway:
sendStatus:
export const sendStatus = (machineName: string) => (status, data = (ctx, evt) => ({})) =>
sendParent((ctx, evt) => ({
type: SubMachineEventType.Update,
data: { machineName, status, data: data(ctx, evt) }
}));
And this is how the parent machine consumes it. I'll only put the relevant parts here:
Main.ts
// ...
export const Chart: MachineConfig<MainContext, MainSchema, MainEvent> = {
initial: "auth",
context: {
auth: {
status: AuthStatus.CheckAuth
},
// ...
user: null,
posUser: null,
posProfile: null
},
states: {
auth: {
on: {
[Base.SubMachineEventType.Update]: { // Just a common "status" event type
target: "auth",
internal: true, // Really important. Make sure you have this, or it won't work
actions: ["updateAuth"]
},
[AuthEventType.FoundSession]: {
target: "cart"
}
},
invoke: {
id: "auth",
src: "auth",
forward: true,
onDone: {
target: "cart",
actions: ["updateAuth"]
},
onError: "main"
}
},
// ...
}
}
export const actions: ActionFunctionMap<MainContext> = {
updateAuthStatus: assign<MainContext>({
auth: (ctx, evt) => ({
...ctx.auth,
...evt.data.data,
status: evt.data.status
})
}),
updateAuth: assign<MainContext>({
user: (ctx, evt) => evt.data.user,
posUser: (ctx, evt) => evt.data.posUser,
posProfile: (ctx, evt) => evt.data.posProfile
}),
// ...
};
export const services: Record<string, ServiceConfig<MainContext>> = {
auth: Auth, // the actual machine
// ...
};
This is how I've been doing it so far. Hopefully you find that helpful. I'm storing the relevant context in the top level, and updating it when the child sends a status event. It might seem like overkill, but for the system I'm building, the upfront cost of having all this planned out will pay dividends for future projects. Because the machines are agnostic to their parents, they can be put into separate modules in reused in other projects.
This is my top level component which should illuminate how I'm dealing with different views based on context and state:
App.tsx
// ...
export interface Props {}
interface State {}
export default class App extends React.Component<Props, State> {
state = {};
render() {
return (
<FSM machine={Main}>
{({ machineState, context, send }) => {
return (
<SwitchState state={machineState}>
<MatchState pred={s => s[0] === "auth"}>
<Auth
email={context.auth.email}
password={context.auth.password}
error={context.auth.error}
send={send}
status={context.auth.status}
/>
</MatchState>
{ /* ... */ }
</SwitchState>
);
}}
</FSM>
);
}
}
I took that pattern from someone else doing something similar. I for the life of me can't find where I originally found it. <FSM>, <SwitchState>, and <MatchState> are just HOC's that conditionally render based on the machine state and context. I could post my code here if you want, but they're pretty straightforward, and I don't wanna put too much code in one comment.
@davidkpiano I'd love to hear your thoughts if you think I'm going the right direction with this. It's currently working just fine on my React Native app. The one thing that's got my a little confused is how to tell the parent machine that the child machine reached an error state. Is there a way to tag a state as like final and error at the same time? Thanks!
When I get this more fleshed out, I intend on compiling this into a doc and/or example repo to help other folks along. I have to get all the grey areas figured out and then remove all the proprietary stuff from my clients first.
@jonlaing thanks for the detailed writeup, did you end up got a chance to publish what you eventually got?
@coodoo No not yet. I'm still working on it and my deadline is coming up fast. I'm almost done though, so shouldn't be too much longer. Thanks for reminding me.
Thanks @jonlaing for the update, very look forward to see your work!
@davidkpiano Sorry, but I don't understand why we can't have 'local' context for each machine too. It is like local variable in the program. Just check iOS 'Time' app. It has many tabs, alarm, timer, world clock, etc. Each tab has own context data.
About restaurant example - it is natural for restaurant app to have separate UI that will display all internal stages of cooking.. In other words, app could contain many different areas and I think it is not good to mix all them together in one global context. This way, context will have the same (or almost the same) structure as its children machines... And a lot of boilerplate to 'send/process' children states to parent.
Hey @aksonov,
I don't understand why we can't have 'local' context for each machine too.
Each machine has it's own private context.
it is natural for restaurant app to have separate UI that will display all internal stages of cooking.. In other words, app could contain many different areas and I think it is not good to mix all them together in one global context
Each area will also have its own behavior and responsibility. You could have a generic timerMachine that is used by the microwaveMachine, ovenMachine or just on it's own. You'd maybe think that those machines need to know about the timer at all times when in reality, they are just concerned about rendering the UI for starting, stopping, adding some extra time to the machine and being notified when the timer is done. Depending on your actor structure, even most of the events I mentioned could be modeled solely within the timerMachine, leaving the parent with only the need to invoke the timerActor and possibly define an event to be notified when it has finished. The actual remaining timer can rightfully be a blackbox that should feel restful in the sense that you need to explicitly communicate to request or change it. This can all be done using actor communication, in this case without even the need to synchronize the timer with the parent.
Therefore, you also won't have any global context. For the actual timer display, you can have a component that subscribes to the private timerActor state and renders it into the UI.
If you are using React, it could look something like this:
const Timer = ({timerRef}) => {
const [state, send] = useService(timerRef);
return <p>{state.context.minutesLeft}:{state.contect.seconds}</p>
}
I learned that oftentimes you don't actually need to synchronize data to a parent, you just need to send more events or think better about the behavior and responsibility of each actor and how they are wired to components.
Let me know if you have more questions and feel free to post a code example on spectrum explaining what you're struggling with. It could really help you and the broader community. I understand how hard it can be to wrap ones head around actors and as far as I know, David is working on a solution to make the experience even better. 馃槉
@CodingDive Thanks for quick response! I'm old fan of state machines and integrated some kind of state machine into my React Native Router Flux v4 library (onEnter, onExit handlers for screens and also transitions to other screens for success/failed promises set by onEnter).
Now I'm working on RNRF v5 and want to integrate it with XState and react-navigation together. So idea is to build navigation state machine for global app navigation and children state machines would be used for separate UI screens, so an user would be able to navigate by sending events to XState and RNRF will listen state machine changes and run proper transitions and needed screens. So now I see that I need to make each UI machine as Actor and also could pass actorRef to that screen
You don't need to go all actors for that. Even though you could and I'm using a ton of actors in my app, every now and then it feels good to break out of the useService chains and use useMachine for passing some services or context. withConfig and withContext are quite powerful so if your only useMachine hook lives at the very root, you're automatically missing out on a lot of goodies.
There are some cases where you want to invoke the machine before you navigate or change the UI for things like prefetching but let's say for a login screen, you can totally get away with using a normal machine and won't need to invoke it by some parent machine first.
```jsx
const Auth = () => {
const [state, send] = useMachine(authMachine);
// ...
}
@CodingDive Thanks, the question is how to use this approach for authentication/registration flow with navigation between different UI screens?
Use case: typical app with auth.
Screens:
The most interesting moments are to check if user is new and to check if auth session is still valid and do some actions with retrieve new session for new credentials.
Also it is interesting how to organize 'stack' structure, i.e. Welcome UI has transitions to Login/Register and when user clicks "Login", we should have two screens in the stack - Welcome and Login. And when users taps "Register", we would have three UI components in the stack - Welcome, Login and Register. I'm trying to achieve that with hierarchical structure, but some ready examples would be very useful..
UPD: Note, that every screen, Login/Register will have own state machines (form error handling, client-server communication, etc.). As I understand you believe that they should not be connected with 'main' state machine? But 'main' state machine will try to do 'auto-login' during startup, so I see many shared actions there
I understand you believe that they should not be connected with 'main' state machine? But 'main' state machine will try to do 'auto-login' during startup, so I see many shared actions there
If that's the case, then model it as actors.
In the application I work on, I have quite some auth setup too. Sign up wizard, login, same component being used in a modal or as a standalone component, social auth, etc. The way I modeled it was to have an AuthMachine at the very top. It conditionally transitions to the sign up or register mode and accordingly invokes their respective machines. The authMachine also takes care of the social auth and either tells the loginMachine to log the user in and transition to a different route/screen, or initializes the sign up wizard by setting some fields from the social auth callback. In fact, because the RegisterMachine was doing so much, I had to introduce another actor that's just responsible for the input like username, email validation, etc. This RegisterInputMachine has no idea about the wizard and which step the user is in. It's parent just sends HIDE and SHOW events for each input. E.g when we receive the social auth callback, we fill in all the inputs we can but want to hide the password field entirely.
I'm sure something similar could work for you. I definitely recommend taking at least a few hours with pen and paper to model it thoroughly. Never realized how much goes into authentication. :)
@aksonov If you're still interested at integrating XState with react-navigation, you should pay a visit to my project at this repo.
Most helpful comment
I'm currently working through this for a project. It's actually a POS system, so checkout is one of the things it has to do. I don't have that part finished, but I can show you how I do authentication, which uses the same nesting machines concept.
Auth.ts
That's not the whole thing, but the rest of it is just type definitions.
sendStatusis just a wrapper aroundsendParentjust to ensure a status in my app always implements a common interface, so we don't get confused with too many different implementations of a status. That said, it might be overkill for your needs. I'll include the code anyway:sendStatus:
And this is how the parent machine consumes it. I'll only put the relevant parts here:
Main.ts
This is how I've been doing it so far. Hopefully you find that helpful. I'm storing the relevant context in the top level, and updating it when the child sends a status event. It might seem like overkill, but for the system I'm building, the upfront cost of having all this planned out will pay dividends for future projects. Because the machines are agnostic to their parents, they can be put into separate modules in reused in other projects.
This is my top level component which should illuminate how I'm dealing with different views based on context and state:
App.tsx
I took that pattern from someone else doing something similar. I for the life of me can't find where I originally found it.
<FSM>,<SwitchState>, and<MatchState>are just HOC's that conditionally render based on the machine state and context. I could post my code here if you want, but they're pretty straightforward, and I don't wanna put too much code in one comment.@davidkpiano I'd love to hear your thoughts if you think I'm going the right direction with this. It's currently working just fine on my React Native app. The one thing that's got my a little confused is how to tell the parent machine that the child machine reached an error state. Is there a way to tag a state as like
finalanderrorat the same time? Thanks!When I get this more fleshed out, I intend on compiling this into a doc and/or example repo to help other folks along. I have to get all the grey areas figured out and then remove all the proprietary stuff from my clients first.