Skip to content

Commit b8ec3b1

Browse files
[core] Add setup(…).extend(…) (#5371)
* WIP extend * Add delays + tests * remove leftover example code in lib code * Address comments * Update test * Fix tests * Add changeset --------- Co-authored-by: Mateusz Burzyński <[email protected]>
1 parent e388c70 commit b8ec3b1

File tree

3 files changed

+473
-68
lines changed

3 files changed

+473
-68
lines changed

.changeset/solid-spies-wink.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'xstate': minor
3+
---
4+
5+
Add `setup.extend()` method to incrementally extend machine setup configurations with additional actions, guards, and delays. This enables composable and reusable machine setups where extended actions, guards, and delays can reference base actions, guards, and delays and support chaining multiple extensions:
6+
7+
```ts
8+
import { setup, not, and } from 'xstate';
9+
10+
const baseSetup = setup({
11+
guards: {
12+
isAuthenticated: () => true,
13+
hasPermission: () => false
14+
}
15+
});
16+
17+
const extendedSetup = baseSetup.extend({
18+
guards: {
19+
// Type-safe guard references
20+
isUnauthenticated: not('isAuthenticated'),
21+
canAccess: and(['isAuthenticated', 'hasPermission'])
22+
}
23+
});
24+
25+
// Both base and extended guards are available
26+
extendedSetup.createMachine({
27+
on: {
28+
LOGIN: {
29+
guard: 'isAuthenticated',
30+
target: 'authenticated'
31+
},
32+
LOGOUT: {
33+
guard: 'isUnauthenticated',
34+
target: 'unauthenticated'
35+
}
36+
}
37+
});
38+
```

packages/core/src/setup.ts

Lines changed: 162 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -79,82 +79,78 @@ type ToProvidedActor<
7979
type RequiredSetupKeys<TChildrenMap> =
8080
IsNever<keyof TChildrenMap> extends true ? never : 'actors';
8181

82-
export function setup<
82+
type SetupReturn<
8383
TContext extends MachineContext,
84-
TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here
85-
TActors extends Record<string, UnknownActorLogic> = {},
86-
TChildrenMap extends Record<string, string> = {},
87-
TActions extends Record<
88-
string,
89-
ParameterizedObject['params'] | undefined
90-
> = {},
91-
TGuards extends Record<
92-
string,
93-
ParameterizedObject['params'] | undefined
94-
> = {},
95-
TDelay extends string = never,
96-
TTag extends string = string,
97-
TInput = NonReducibleUnknown,
98-
TOutput extends NonReducibleUnknown = NonReducibleUnknown,
99-
TEmitted extends EventObject = EventObject,
100-
TMeta extends MetaObject = MetaObject
101-
>({
102-
schemas,
103-
actors,
104-
actions,
105-
guards,
106-
delays
107-
}: {
108-
schemas?: unknown;
109-
types?: SetupTypes<
84+
TEvent extends AnyEventObject,
85+
TActors extends Record<string, UnknownActorLogic>,
86+
TChildrenMap extends Record<string, string>,
87+
TActions extends Record<string, ParameterizedObject['params'] | undefined>,
88+
TGuards extends Record<string, ParameterizedObject['params'] | undefined>,
89+
TDelay extends string,
90+
TTag extends string,
91+
TInput,
92+
TOutput extends NonReducibleUnknown,
93+
TEmitted extends EventObject,
94+
TMeta extends MetaObject
95+
> = {
96+
extend: <
97+
TExtendActions extends Record<
98+
string,
99+
ParameterizedObject['params'] | undefined
100+
> = {},
101+
TExtendGuards extends Record<
102+
string,
103+
ParameterizedObject['params'] | undefined
104+
> = {},
105+
TExtendDelays extends string = never
106+
>({
107+
actions,
108+
guards,
109+
delays
110+
}: {
111+
actions?: {
112+
[K in keyof TExtendActions]: ActionFunction<
113+
TContext,
114+
TEvent,
115+
TEvent,
116+
TExtendActions[K],
117+
ToProvidedActor<TChildrenMap, TActors>,
118+
ToParameterizedObject<TActions & TExtendActions>,
119+
ToParameterizedObject<TGuards & TExtendGuards>,
120+
TDelay | TExtendDelays,
121+
TEmitted
122+
>;
123+
};
124+
guards?: {
125+
[K in keyof TExtendGuards]: GuardPredicate<
126+
TContext,
127+
TEvent,
128+
TExtendGuards[K],
129+
ToParameterizedObject<TGuards & TExtendGuards>
130+
>;
131+
};
132+
delays?: {
133+
[K in TExtendDelays]: DelayConfig<
134+
TContext,
135+
TEvent,
136+
ToParameterizedObject<TActions & TExtendActions>['params'],
137+
TEvent
138+
>;
139+
};
140+
}) => SetupReturn<
110141
TContext,
111142
TEvent,
143+
TActors,
112144
TChildrenMap,
145+
TActions & TExtendActions,
146+
TGuards & TExtendGuards,
147+
TDelay | TExtendDelays,
113148
TTag,
114149
TInput,
115150
TOutput,
116151
TEmitted,
117152
TMeta
118153
>;
119-
actors?: {
120-
// union here enforces that all configured children have to be provided in actors
121-
// it makes those values required here
122-
[K in keyof TActors | Values<TChildrenMap>]: K extends keyof TActors
123-
? TActors[K]
124-
: never;
125-
};
126-
actions?: {
127-
[K in keyof TActions]: ActionFunction<
128-
TContext,
129-
TEvent,
130-
TEvent,
131-
TActions[K],
132-
ToProvidedActor<TChildrenMap, TActors>,
133-
ToParameterizedObject<TActions>,
134-
ToParameterizedObject<TGuards>,
135-
TDelay,
136-
TEmitted
137-
>;
138-
};
139-
guards?: {
140-
[K in keyof TGuards]: GuardPredicate<
141-
TContext,
142-
TEvent,
143-
TGuards[K],
144-
ToParameterizedObject<TGuards>
145-
>;
146-
};
147-
delays?: {
148-
[K in TDelay]: DelayConfig<
149-
TContext,
150-
TEvent,
151-
ToParameterizedObject<TActions>['params'],
152-
TEvent
153-
>;
154-
};
155-
} & {
156-
[K in RequiredSetupKeys<TChildrenMap>]: unknown;
157-
}): {
158154
/**
159155
* Creates a state config that is strongly typed. This state config can be
160156
* used to create a machine.
@@ -327,7 +323,97 @@ export function setup<
327323
TEvent,
328324
ToProvidedActor<TChildrenMap, TActors>
329325
>;
330-
} {
326+
};
327+
328+
export function setup<
329+
TContext extends MachineContext,
330+
TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here
331+
TActors extends Record<string, UnknownActorLogic> = {},
332+
TChildrenMap extends Record<string, string> = {},
333+
TActions extends Record<
334+
string,
335+
ParameterizedObject['params'] | undefined
336+
> = {},
337+
TGuards extends Record<
338+
string,
339+
ParameterizedObject['params'] | undefined
340+
> = {},
341+
TDelay extends string = never,
342+
TTag extends string = string,
343+
TInput = NonReducibleUnknown,
344+
TOutput extends NonReducibleUnknown = NonReducibleUnknown,
345+
TEmitted extends EventObject = EventObject,
346+
TMeta extends MetaObject = MetaObject
347+
>({
348+
schemas,
349+
actors,
350+
actions,
351+
guards,
352+
delays
353+
}: {
354+
schemas?: unknown;
355+
types?: SetupTypes<
356+
TContext,
357+
TEvent,
358+
TChildrenMap,
359+
TTag,
360+
TInput,
361+
TOutput,
362+
TEmitted,
363+
TMeta
364+
>;
365+
actors?: {
366+
// union here enforces that all configured children have to be provided in actors
367+
// it makes those values required here
368+
[K in keyof TActors | Values<TChildrenMap>]: K extends keyof TActors
369+
? TActors[K]
370+
: never;
371+
};
372+
actions?: {
373+
[K in keyof TActions]: ActionFunction<
374+
TContext,
375+
TEvent,
376+
TEvent,
377+
TActions[K],
378+
ToProvidedActor<TChildrenMap, TActors>,
379+
ToParameterizedObject<TActions>,
380+
ToParameterizedObject<TGuards>,
381+
TDelay,
382+
TEmitted
383+
>;
384+
};
385+
guards?: {
386+
[K in keyof TGuards]: GuardPredicate<
387+
TContext,
388+
TEvent,
389+
TGuards[K],
390+
ToParameterizedObject<TGuards>
391+
>;
392+
};
393+
delays?: {
394+
[K in TDelay]: DelayConfig<
395+
TContext,
396+
TEvent,
397+
ToParameterizedObject<TActions>['params'],
398+
TEvent
399+
>;
400+
};
401+
} & {
402+
[K in RequiredSetupKeys<TChildrenMap>]: unknown;
403+
}): SetupReturn<
404+
TContext,
405+
TEvent,
406+
TActors,
407+
TChildrenMap,
408+
TActions,
409+
TGuards,
410+
TDelay,
411+
TTag,
412+
TInput,
413+
TOutput,
414+
TEmitted,
415+
TMeta
416+
> {
331417
return {
332418
assign,
333419
sendTo,
@@ -349,6 +435,14 @@ export function setup<
349435
guards,
350436
delays
351437
}
352-
)
438+
),
439+
extend: (extended) =>
440+
setup({
441+
schemas,
442+
actors,
443+
actions: { ...actions, ...extended.actions },
444+
guards: { ...guards, ...extended.guards },
445+
delays: { ...delays, ...extended.delays }
446+
} as any)
353447
};
354448
}

0 commit comments

Comments
 (0)