Share actors
Provide one actor to a React subtree.
XState v6 is in alpha
createActorContext(logic, options?) builds a React context around actor logic. Every component under the provider reads and sends to the same actor.
import { createActorContext } from '@xstate/react';
export const CheckoutContext = createActorContext(checkoutMachine);
function App() {
return (
<CheckoutContext.Provider>
<Cart />
<PaymentStep />
</CheckoutContext.Provider>
);
}
function Cart() {
const total = CheckoutContext.useSelector((snapshot) => snapshot.context.total);
const actorRef = CheckoutContext.useActorRef();
return <button onClick={() => actorRef.trigger.pay()}>Pay {total}</button>;
}The returned object has three members.
| Member | Description |
|---|---|
Provider | Creates one actor and provides it to its subtree. |
useSelector(selector, compare?) | Reads a derived value from the provided actor. Same semantics as useSelector. |
useActorRef() | Returns the provided actor reference. Does not re-render. |
Each rendered Provider creates its own actor, with the same lifecycle as useActorRef: started on mount, stopped on unmount, recreated if it was stopped while the component stayed mounted.
Calling useSelector or useActorRef outside the provider throws: You used a hook from "ActorProvider" but it's not inside a <ActorProvider> component.
Provider options
Provider takes an options prop with the same actor options as createActor(...): input, snapshot, id, inspect, and the rest.
<CheckoutContext.Provider options={{ input: { orderId } }}>
<Checkout />
</CheckoutContext.Provider>Options passed to createActorContext(logic, options) are defaults. The options prop is merged over them, key by key.
const CheckoutContext = createActorContext(checkoutMachine, {
inspect: inspector
});
// inspect is kept, input is added
<CheckoutContext.Provider options={{ input: { orderId } }} />;logic overrides the logic for one provider, which is how a test swaps in machine.provide({ ... }).
<CheckoutContext.Provider
logic={checkoutMachine.provide({ actors: { authorize: fakeAuthorize } })}
>
<Checkout />
</CheckoutContext.Provider>Warning: The
machineprop was removed. Passing it throws. Uselogic.
Provide implementations
A provider component can take implementations as props and pass them to the shared actor. Build the logic with machine.provide({ ... }) and pass it as logic; pass input through options.
import * as React from 'react';
import { createActorContext } from '@xstate/react';
import { checkoutMachine } from './checkoutMachine';
import type { authorize } from './authorize';
export const CheckoutContext = createActorContext(checkoutMachine);
export function CheckoutProvider({
orderId,
authorizeLogic,
track,
children
}: {
orderId: string;
authorizeLogic: typeof authorize;
track: (params: { name: string }) => void;
children: React.ReactNode;
}) {
const [logic] = React.useState(() =>
checkoutMachine.provide({
actors: { authorize: authorizeLogic },
actions: { track }
})
);
return (
<CheckoutContext.Provider logic={logic} options={{ input: { orderId } }}>
{children}
</CheckoutContext.Provider>
);
}The logic is created once, on the first render, and the actor runs with those implementations for the provider's lifetime. To switch to different implementations, change the provider's key. React unmounts the old provider, which stops its actor, and mounts a new provider with a new actor.
<CheckoutProvider
key={tenant.id}
orderId={orderId}
authorizeLogic={tenant.authorize}
track={tenant.track}
>
<Checkout />
</CheckoutProvider>The new actor starts from the machine's initial state. Consumers keep calling CheckoutContext.useSelector(...) and CheckoutContext.useActorRef() unchanged.
When to use it
Use a provider when several components in one subtree need the same actor and the actor's lifetime matches a piece of the UI: a cart shared across checkout steps, a wizard shared across its steps, a media player shared by transport controls and a timeline.
Prefer passing the actor reference as a prop when only one or two components need it. useActorRef in a parent plus useSelector in a child is simpler and more explicit than a context.
Place the provider close to the feature it belongs to. A provider at the application root gives every instance of that feature the same actor, which is rarely what you want for per-item state. For state that belongs to the whole application, see global state.
TypeScript
Snapshot, event and actor reference types come from the logic.
input stays optional on createActorContext and on the Provider options prop, even when the logic requires input, because the actor's options are merged from the createActorContext(logic, options) defaults and the <Provider options> prop. A missing required input is not a type error; when initialization reads it, the actor starts with an error snapshot.
const CheckoutContext = createActorContext(checkoutMachine);
const total = CheckoutContext.useSelector((s) => s.context.total); // number
const actorRef = CheckoutContext.useActorRef(); // Actor<typeof checkoutMachine>Shared actors cheatsheet
const Ctx = createActorContext(logic, defaultOptions);
<Ctx.Provider options={{ input, snapshot }} logic={overrideLogic}>
{children}
</Ctx.Provider>;
Ctx.useSelector((snapshot) => snapshot.context.value);
Ctx.useSelector((snapshot) => snapshot.context.user, shallowEqual);
Ctx.useActorRef();