TL;DR
If you just want to see the code, it is here. And this is the PR with the latest changes that are discussed in the post.
Background
This post drifts a bit from the series, but it’s something I had been postponing for a while after switching from v4 to v5. I didn't find it that important at the time, but I've finally started deprecating the sendParent
action.
Pros and cons
As stated in the xState documentation, the sendParent
is no longer the preferred way to implement the child to parent
communication in your machines.
There are couple of things that stopped me from doing this earlier with the adoption of v5.
I've had enough codebase to refactor and the still working sendParent
didn’t feel like a priority.
Passing the parent
as input
and keeping it in the context felt like too much boilerplate.
At the time, I also had a mental barrier around how the parent
is typed in the child machine. I expected to be able to reuse the already existing type of the parent machine, but this led to TypeScript errors beyond my knowledge and expertise. After a discussion in the XState Discord channel, this solution surfaced, though at the time, it felt hacky to me:
type ParentActor = ActorRef<Snapshot<unknown>, ParentEvents>;
However, I kept encountering bugs due to the poor TypeScript support of sendParent
.
Eventually, this example was added to the XState documentation — and it finally gave me the confidence to switch:
type ChildEvent = {
type: 'tellParentSomething';
data?: string;
};
type ParentActor = ActorRef<Snapshot<unknown>, ChildEvent>;
const childMachine = setup({
types: {
context: {} as {
parentRef: ParentActor;
},
input: {} as {
parentRef: ParentActor;
},
},
}).createMachine({
context: ({ input: { parentRef } }) => ({ parentRef }),
entry: sendTo(({ context }) => context.parentRef, {
type: 'tellParentSomething',
data: 'Hi parent!',
}),
});
export const parent = setup({
actors: { child: childMachine },
}).createMachine({
invoke: {
src: 'child',
input: ({ self }) => ({
parentRef: self,
}),
},
on: {
tellParentSomething: {
actions: log(({ event: { data } }) => `Child actor says "${data}"`),
},
},
});
Mapping this approach to my codebase looks like this:
// This is the parent
export type Events =
| { type: "START_APP" }
| { type: "SIGN_IN"; username: string }
| { type: "SIGN_OUT" }
| { type: "GET_USER" };
export const appMachine = setup({
types: {
events: {} as Events,
context: {} as {
refAuthenticated: AuthenticatedMachineActor | undefined;
},
},
actors: {
authenticatedMachine,
},
actions: {
setRefAuthenticated: assign({
refAuthenticated: ({ spawn, self }) => {
return spawn("authenticatedMachine", {
id: "authenticatedMachine",
input: { parent: self },
});
},
}),
stopRefAuthenticated: assign({ refAuthenticated: undefined }),
},
}).createMachine({
id: "application",
initial: "initializing",
context: {
refAuthenticated: undefined,
},
states: {
// ...
authenticated: {
entry: ["setRefAuthenticated"],
on: { SIGN_OUT: { target: "signingOut" } },
exit: [stopChild("authenticatedMachine"), "stopRefAuthenticated"],
},
// ...
},
});
export const AppContext = createActorContext(appMachine);
// This is the child
import { Events as ParentEvents } from "../contexts/useApp";
type ParentActor = ActorRef<Snapshot<unknown>, ParentEvents>;
export type AuthenticatedMachineActor = ActorRefFrom<
typeof authenticatedMachine
>;
export const authenticatedMachine = setup({
types: {
input: {} as { parent: ParentActor },
context: {} as {
refParent: ParentActor;
},
events: {} as
// ...
| { type: "SIGN_OUT" },
},
actions: {
sendParentSignOut({ context }) {
context.refParent.send({ type: "SIGN_OUT" });
},
},
}).createMachine({
context: ({ input }) => {
return { refParent: input.parent, refHome: undefined, refList: undefined };
},
id: "authenticatedNavigator",
// ...
on: {
// ...
SIGN_OUT: { actions: ["sendParentSignOut"] },
},
states: {
// ...
},
});
One subtle difference from the documentation is that instead of using the sendTo
action, I directly call context.refParent.send(...)
.
Also, in order to keep my codebase consistent, I try to always keep the the event types in the same file as the machine and export them as Events
. Later, when needed in the child machine, I alias them as desired (ParentEvents
, RootEvents
, etc.)
Conclusion
I'm still not excited about passing and storing parent references in the context, but the improved typing significantly enhances the developer experience (DX).
Top comments (0)