Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
316edcb
Add route + tests
davidkpiano Aug 8, 2023
df8ed86
Add route tests
davidkpiano Aug 9, 2023
4c83916
Merge branch 'main' into v5/routes
davidkpiano Jan 2, 2024
b36eb37
Fix types
davidkpiano Jan 2, 2024
f5307a9
Merge branch 'main' into v5/routes
davidkpiano Feb 5, 2024
abb56ec
Merge branch 'main' into v5/routes
davidkpiano Mar 7, 2024
1b6346e
Merge branch 'main' into v5/routes
davidkpiano May 19, 2024
d0f66d1
Fix types
davidkpiano May 19, 2024
447e978
One down
davidkpiano Jun 20, 2024
2b66bc4
Default to typegen disabled
davidkpiano Jun 20, 2024
b134d0d
Remove stuff
davidkpiano Jun 20, 2024
4378324
More progress
davidkpiano Jun 20, 2024
1f56ae7
Some surgery
davidkpiano Jun 20, 2024
f428bf1
Actors
davidkpiano Jun 20, 2024
3778177
This is fun
davidkpiano Jun 20, 2024
4c8202c
This is fun
davidkpiano Jun 20, 2024
480b646
More deletion
davidkpiano Jun 20, 2024
2ade975
Remove ResolveTypegenMeta type param
davidkpiano Jun 20, 2024
752b50a
Renaming
davidkpiano Jun 20, 2024
b657858
Cleanup
davidkpiano Jun 20, 2024
24104b5
Remove typgenTypes files + mentions
davidkpiano Jun 21, 2024
3158807
Merge branch 'main' into davidkpiano/remove-typegen-1
davidkpiano Jun 22, 2024
7410d0a
Simplification
davidkpiano Jun 22, 2024
e71693b
Rename TResolvedTypesMeta -> TTypes
davidkpiano Jun 22, 2024
99843ed
More simplification
davidkpiano Jun 22, 2024
751f7e0
Strongly type meta keyes
davidkpiano Jul 3, 2024
4022c9c
Merge branch 'main' into v5/routes
davidkpiano Jul 5, 2024
22ae7b7
Merge branch 'main' into davidkpiano/getmeta-typed
davidkpiano Jul 10, 2024
cc83948
Add test
davidkpiano Jul 10, 2024
6de31d3
Clean up State
davidkpiano Jul 10, 2024
6e6950b
Merge branch 'main' into v5/routes
davidkpiano Jul 11, 2024
38041c3
Merge branch 'davidkpiano/getmeta-typed' into v5/routes
davidkpiano Jul 12, 2024
6c4843e
Consistency: use full state IDs
davidkpiano Jul 12, 2024
ca80978
Strongly type route events
davidkpiano Jul 12, 2024
4e3a800
Merge branch 'main' into v5/routes
davidkpiano Sep 18, 2024
4893ba6
Merge branch 'main' into v5/routes
davidkpiano Dec 2, 2024
2604d70
Merge branch 'main' into v5/routes
davidkpiano May 1, 2025
1a4b773
Merge branch 'main' into v5/routes
davidkpiano Oct 9, 2025
69de11b
Merge branch 'main' into v5/routes
davidkpiano Feb 5, 2026
db3f8fa
Deep routing
davidkpiano Feb 5, 2026
a028ff8
Changeset
davidkpiano Feb 5, 2026
9d02e91
Route by id with single event type
davidkpiano Feb 10, 2026
cf31cfd
Remove reenter prop
davidkpiano Feb 10, 2026
4e09eed
Move route transition formatting to root
davidkpiano Feb 10, 2026
65fb9e6
Update packages/core/src/types.ts
davidkpiano Feb 12, 2026
01d3fff
Update routable states to use '#' prefix for navigation and adjust re…
davidkpiano Feb 12, 2026
2788fb2
Add test
davidkpiano Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/three-sails-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'xstate': minor
---

Added routable states. States with `route: {}` and an explicit `id` can be navigated to from anywhere via a single `{ type: 'xstate.route', to: '#id' }` event.

```ts
const machine = setup({}).createMachine({
id: 'app',
initial: 'home',
states: {
home: { id: 'home', route: {} },
dashboard: {
initial: 'overview',
states: {
overview: { id: 'overview', route: {} },
settings: { id: 'settings', route: {} }
}
}
}
});

const actor = createActor(machine).start();

// Route directly to deeply nested state from anywhere
actor.send({ type: 'xstate.route', to: '#settings' });
```

Routes support guards for conditional navigation:

```ts
settings: {
id: 'settings',
route: {
guard: ({ context }) => context.role === 'admin'
}
}
```
2 changes: 2 additions & 0 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from './State.ts';
import { StateNode } from './StateNode.ts';
import {
formatRouteTransitions,
getAllStateNodes,
getInitialStateNodes,
getStateNodeByPath,
Expand Down Expand Up @@ -148,6 +149,7 @@ export class StateMachine<
});

this.root._initialize();
formatRouteTransitions(this.root);

this.states = this.root.states; // TODO: remove!
this.events = this.root.events;
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
MetaObject,
NonReducibleUnknown,
ParameterizedObject,
RoutableStateId,
SetupTypes,
StateNodeConfig,
StateSchema,
Expand Down Expand Up @@ -251,7 +252,13 @@ type SetupReturn<
config: TConfig
) => StateMachine<
TContext,
TEvent,
| TEvent
| ([RoutableStateId<TConfig>] extends [never]
? never
: {
type: 'xstate.route';
to: RoutableStateId<TConfig>;
}),
Cast<
ToChildren<ToProvidedActor<TChildrenMap, TActors>>,
Record<string, AnyActorRef | undefined>
Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,53 @@ export function formatTransitions<
return transitions as Map<string, TransitionDefinition<TContext, any>[]>;
}

/**
* Collects route transitions from all descendants with explicit IDs. Called
* once on the root node to avoid O(N²) repeated traversals.
*/
export function formatRouteTransitions(rootStateNode: AnyStateNode): void {
const routeTransitions: AnyTransitionDefinition[] = [];
const collectRoutes = (states: Record<string, AnyStateNode>) => {
Object.values(states).forEach((sn) => {
if (sn.config.route && sn.config.id) {
const routeId = sn.config.id;
const userGuard = sn.config.route.guard;
const routeGuard = (
args: { context: any; event: any },
params: any
) => {
if (args.event.to !== `#${routeId}`) {
return false;
}
if (!userGuard) {
return true;
}
if (typeof userGuard === 'function') {
return userGuard(args, params);
}
return true;
};
const transition: AnyTransitionConfig = {
...sn.config.route,
guard: routeGuard,
target: `#${routeId}`
};

routeTransitions.push(
formatTransition(rootStateNode, 'xstate.route', transition)
);
}
if (sn.states) {
collectRoutes(sn.states);
}
});
};
collectRoutes(rootStateNode.states);
if (routeTransitions.length > 0) {
rootStateNode.transitions.set('xstate.route', routeTransitions);
}
}

export function formatInitialTransition<
TContext extends MachineContext,
TEvent extends EventObject
Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,42 @@ export interface StateNodeConfig<

/** A default target for a history state */
target?: string | undefined; // `| undefined` makes `HistoryStateNodeConfig` compatible with this interface (it extends it) under `exactOptionalPropertyTypes`
route?: RouteTransitionConfig<
TContext,
TEvent,
TEvent,
TActor,
TAction,
TGuard,
TDelay,
TEmitted
>;
}

export interface RouteTransitionConfig<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject,
TActor extends ProvidedActor,
TAction extends ParameterizedObject,
TGuard extends ParameterizedObject,
TDelay extends string,
TEmitted extends EventObject
> {
guard?: Guard<TContext, TExpressionEvent, undefined, TGuard>;
actions?: Actions<
TContext,
TExpressionEvent,
TEvent,
undefined,
TActor,
TAction,
TGuard,
TDelay,
TEmitted
>;
meta?: Record<string, any>;
description?: string;
}

export type AnyStateNodeConfig = StateNodeConfig<
Expand Down Expand Up @@ -2499,6 +2535,7 @@ export type ToChildren<TActor extends ProvidedActor> =

export type StateSchema = {
id?: string;
route?: unknown;
states?: Record<string, StateSchema>;

// Other types
Expand Down Expand Up @@ -2542,6 +2579,16 @@ export type StateId<
}>
: never);

export type RoutableStateId<TSchema extends StateSchema> =
| (TSchema extends { route: any; id: string } ? `#${TSchema['id']}` : never)
| (TSchema['states'] extends Record<string, any>
? Values<{
[K in keyof TSchema['states'] & string]: RoutableStateId<
TSchema['states'][K]
>;
}>
: never);

export interface StateMachineTypes {
context: MachineContext;
events: EventObject;
Expand Down
Loading