Skip to content

Commit 15e15b5

Browse files
[core] Add .createStateConfig(…) to machine setup (#5364)
* Add createStateConfig method to setup for strongly typed state configuration * Add changeset * add tests * make it generic --------- Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
1 parent 7dddf17 commit 15e15b5

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed

.changeset/four-horses-shine.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'xstate': minor
3+
---
4+
5+
Added `.createStateConfig(…)` to the setup API. This makes it possible to create state configs that are strongly typed and modular.
6+
7+
```ts
8+
const lightMachineSetup = setup({
9+
// ...
10+
});
11+
12+
const green = lightMachineSetup.createStateConfig({
13+
//...
14+
});
15+
16+
const yellow = lightMachineSetup.createStateConfig({
17+
//...
18+
});
19+
20+
const red = lightMachineSetup.createStateConfig({
21+
//...
22+
});
23+
24+
const machine = lightMachineSetup.createMachine({
25+
initial: 'green',
26+
states: {
27+
green,
28+
yellow,
29+
red
30+
}
31+
});
32+
```

packages/core/src/setup.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
NonReducibleUnknown,
1818
ParameterizedObject,
1919
SetupTypes,
20+
StateNodeConfig,
2021
ToChildren,
2122
ToStateValue,
2223
UnknownActorLogic,
@@ -145,6 +146,51 @@ export function setup<
145146
} & {
146147
[K in RequiredSetupKeys<TChildrenMap>]: unknown;
147148
}): {
149+
/**
150+
* Creates a state config that is strongly typed. This state config can be
151+
* used to create a machine.
152+
*
153+
* @example
154+
*
155+
* ```ts
156+
* const lightMachineSetup = setup({
157+
* // ...
158+
* });
159+
*
160+
* const green = lightMachineSetup.createStateConfig({
161+
* on: {
162+
* timer: {
163+
* actions: 'doSomething'
164+
* }
165+
* }
166+
* });
167+
*
168+
* const machine = lightMachineSetup.createMachine({
169+
* initial: 'green',
170+
* states: {
171+
* green,
172+
* yellow,
173+
* red
174+
* }
175+
* });
176+
* ```
177+
*/
178+
createStateConfig: <
179+
TStateConfig extends StateNodeConfig<
180+
TContext,
181+
TEvent,
182+
ToProvidedActor<TChildrenMap, TActors>,
183+
ToParameterizedObject<TActions>,
184+
ToParameterizedObject<TGuards>,
185+
TDelay,
186+
TTag,
187+
unknown,
188+
TEmitted,
189+
TMeta
190+
>
191+
>(
192+
config: TStateConfig
193+
) => TStateConfig;
148194
createMachine: <
149195
const TConfig extends MachineConfig<
150196
TContext,
@@ -182,6 +228,7 @@ export function setup<
182228
>;
183229
} {
184230
return {
231+
createStateConfig: (config) => config,
185232
createMachine: (config) =>
186233
(createMachine as any)(
187234
{ ...config, schemas },

packages/core/test/setup.types.test.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,3 +2398,253 @@ describe('setup()', () => {
23982398
((_accept: ContextFrom<typeof machine>) => {})({ myVar: 'whatever' });
23992399
});
24002400
});
2401+
2402+
describe('createStateConfig', () => {
2403+
it('should be able to create a state config with a custom action', () => {
2404+
const machineSetup = setup({
2405+
types: {
2406+
context: {} as {
2407+
count: number;
2408+
},
2409+
events: {} as {
2410+
type: 'timer';
2411+
by: number;
2412+
}
2413+
},
2414+
actions: {
2415+
doSomething: () => {}
2416+
},
2417+
guards: {
2418+
isLightActive: () => true
2419+
}
2420+
});
2421+
2422+
const green = machineSetup.createStateConfig({
2423+
on: {
2424+
timer: {
2425+
actions: 'doSomething',
2426+
guard: 'isLightActive'
2427+
}
2428+
}
2429+
});
2430+
2431+
const yellow = machineSetup.createStateConfig({
2432+
on: {
2433+
timer: {
2434+
actions: 'doSomething',
2435+
guard: 'isLightActive'
2436+
}
2437+
}
2438+
});
2439+
2440+
const red = machineSetup.createStateConfig({
2441+
on: {
2442+
timer: {
2443+
actions: 'doSomething',
2444+
guard: 'isLightActive'
2445+
}
2446+
}
2447+
});
2448+
2449+
const invalidEvent = machineSetup.createStateConfig({
2450+
on: {
2451+
// @ts-expect-error
2452+
nonsense: {}
2453+
}
2454+
});
2455+
2456+
const invalidAction = machineSetup.createStateConfig({
2457+
on: {
2458+
// @ts-expect-error
2459+
timer: {
2460+
// TODO: why is the error not here?
2461+
actions: 'nonexistent'
2462+
}
2463+
}
2464+
});
2465+
2466+
const invalidGuard = machineSetup.createStateConfig({
2467+
on: {
2468+
// @ts-expect-error
2469+
timer: {
2470+
// TODO: why is the error not here?
2471+
guard: 'nonexistent'
2472+
}
2473+
}
2474+
});
2475+
2476+
machineSetup.createMachine({
2477+
context: {
2478+
count: 0
2479+
},
2480+
initial: 'green',
2481+
states: {
2482+
green,
2483+
yellow,
2484+
red,
2485+
invalidEvent,
2486+
invalidAction,
2487+
invalidGuard
2488+
}
2489+
});
2490+
});
2491+
2492+
it('should allow matching against valid top state keys of a statechart with nested compound states', () => {
2493+
const machineSetup = setup({});
2494+
const green = machineSetup.createStateConfig({
2495+
initial: 'walk',
2496+
states: {
2497+
walk: {},
2498+
wait: {}
2499+
}
2500+
});
2501+
const machine = machineSetup.createMachine({
2502+
initial: 'green',
2503+
states: {
2504+
green,
2505+
yellow: {},
2506+
red: {}
2507+
}
2508+
});
2509+
2510+
const snapshot = createActor(machine).start().getSnapshot();
2511+
2512+
snapshot.matches('green');
2513+
snapshot.matches('yellow');
2514+
snapshot.matches('red');
2515+
});
2516+
2517+
it('should not allow matching against an invalid top state key of a statechart with nested compound states', () => {
2518+
const machineSetup = setup({});
2519+
const green = machineSetup.createStateConfig({
2520+
initial: 'walk',
2521+
states: {
2522+
walk: {},
2523+
wait: {}
2524+
}
2525+
});
2526+
const machine = machineSetup.createMachine({
2527+
initial: 'green',
2528+
states: {
2529+
green,
2530+
yellow: {},
2531+
red: {}
2532+
}
2533+
});
2534+
2535+
const snapshot = createActor(machine).start().getSnapshot();
2536+
2537+
snapshot.matches(
2538+
// @ts-expect-error
2539+
'orange'
2540+
);
2541+
});
2542+
2543+
it('should allow matching against a valid full object value of a statechart with nested compound states', () => {
2544+
const machineSetup = setup({});
2545+
const green = machineSetup.createStateConfig({
2546+
initial: 'walk',
2547+
states: {
2548+
walk: {},
2549+
wait: {}
2550+
}
2551+
});
2552+
const machine = machineSetup.createMachine({
2553+
initial: 'green',
2554+
states: {
2555+
green,
2556+
yellow: {},
2557+
red: {}
2558+
}
2559+
});
2560+
2561+
const snapshot = createActor(machine).start().getSnapshot();
2562+
2563+
snapshot.matches({
2564+
green: 'wait'
2565+
});
2566+
});
2567+
2568+
it('should allow matching against a valid non-full object value of a statechart with nested compound states', () => {
2569+
const machineSetup = setup({});
2570+
const green = machineSetup.createStateConfig({
2571+
initial: 'walk',
2572+
states: {
2573+
walk: {
2574+
initial: 'steady',
2575+
states: {
2576+
steady: {},
2577+
slowingDown: {}
2578+
}
2579+
},
2580+
wait: {}
2581+
}
2582+
});
2583+
const machine = machineSetup.createMachine({
2584+
initial: 'green',
2585+
states: {
2586+
green,
2587+
yellow: {},
2588+
red: {}
2589+
}
2590+
});
2591+
2592+
const snapshot = createActor(machine).start().getSnapshot();
2593+
2594+
snapshot.matches({
2595+
green: 'wait'
2596+
});
2597+
});
2598+
2599+
it('should not allow matching against a invalid object value of a statechart with nested compound states', () => {
2600+
const machineSetup = setup({});
2601+
const green = machineSetup.createStateConfig({
2602+
initial: 'walk',
2603+
states: {
2604+
walk: {},
2605+
wait: {}
2606+
}
2607+
});
2608+
const machine = machineSetup.createMachine({
2609+
initial: 'green',
2610+
states: {
2611+
green,
2612+
yellow: {},
2613+
red: {}
2614+
}
2615+
});
2616+
2617+
const snapshot = createActor(machine).start().getSnapshot();
2618+
2619+
snapshot.matches({
2620+
// @ts-expect-error
2621+
green: 'invalid'
2622+
});
2623+
});
2624+
2625+
it('should not allow matching against a invalid object value with self-key at value position', () => {
2626+
const machineSetup = setup({});
2627+
const green = machineSetup.createStateConfig({
2628+
initial: 'walk',
2629+
states: {
2630+
walk: {},
2631+
wait: {}
2632+
}
2633+
});
2634+
const machine = machineSetup.createMachine({
2635+
initial: 'green',
2636+
states: {
2637+
green,
2638+
yellow: {},
2639+
red: {}
2640+
}
2641+
});
2642+
2643+
const snapshot = createActor(machine).start().getSnapshot();
2644+
2645+
snapshot.matches({
2646+
// @ts-expect-error
2647+
green: 'green'
2648+
});
2649+
});
2650+
});

0 commit comments

Comments
 (0)