Skip to content

Commit 0a6ffcd

Browse files
committed
feat(xstate-tree): allow supplying input with buildRootComponent
Updates the buildRootComponent function to allow specifying input to be passed to the actor when it's created To enable proper typescript support this required changing the signature of buildRootComponent to accept a single object which contains the machine, optionally routing information, and input depending on whether the machine defines an input type BREAKING CHANGE: buildRootComponent now accepts a single required options object instead of a required machine and optional options object
1 parent e3c427b commit 0a6ffcd

13 files changed

+163
-91
lines changed

examples/todomvc/index.tsx

+7-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { routes, history } from "./routes";
77

88
const appRoot = document.getElementById("root");
99
const root = createRoot(appRoot!);
10-
const App = buildRootComponent(TodoApp, {
11-
basePath: "/",
12-
history,
13-
routes,
10+
const App = buildRootComponent({
11+
machine: TodoApp,
12+
routing: {
13+
basePath: "/",
14+
history,
15+
routes,
16+
},
1417
});
1518

1619
root.render(<App />);

src/builders.spec.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe("xstate-tree builders", () => {
1111
describe("viewToMachine", () => {
1212
it("takes a React view and wraps it in an xstate-tree machine that renders that view", async () => {
1313
const ViewMachine = viewToMachine(() => <div>hello world</div>);
14-
const Root = buildRootComponent(ViewMachine);
14+
const Root = buildRootComponent({ machine: ViewMachine });
1515

1616
const { getByText } = render(<Root />);
1717

@@ -41,10 +41,13 @@ describe("xstate-tree builders", () => {
4141
GO_TO_BAR: BarMachine,
4242
});
4343

44-
const Root = buildRootComponent(routingMachine, {
45-
history: hist,
46-
basePath: "/",
47-
routes: [fooRoute, barRoute],
44+
const Root = buildRootComponent({
45+
machine: routingMachine,
46+
routing: {
47+
history: hist,
48+
basePath: "/",
49+
routes: [fooRoute, barRoute],
50+
},
4851
});
4952

5053
const { getByText } = render(<Root />);

src/lazy.spec.tsx

+10-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe("lazy", () => {
1919
it("renders null by default when loading", () => {
2020
const promiseFactory = () => new Promise<any>(() => void 0);
2121
const lazyMachine = lazy(promiseFactory);
22-
const Root = buildRootComponent(lazyMachine);
22+
const Root = buildRootComponent({ machine: lazyMachine });
2323

2424
const { container, rerender } = render(<Root />);
2525
rerender(<Root />);
@@ -32,7 +32,7 @@ describe("lazy", () => {
3232
const lazyMachine = lazy(promiseFactory, {
3333
Loader: () => <p>loading</p>,
3434
});
35-
const Root = buildRootComponent(lazyMachine);
35+
const Root = buildRootComponent({ machine: lazyMachine });
3636

3737
const { container, rerender } = render(<Root />);
3838
rerender(<Root />);
@@ -76,14 +76,14 @@ describe("lazy", () => {
7676
});
7777
const slots = [lazyMachineSlot];
7878

79-
const Root = buildRootComponent(
80-
createXStateTreeMachine(rootMachine, {
79+
const Root = buildRootComponent({
80+
machine: createXStateTreeMachine(rootMachine, {
8181
slots,
8282
View({ slots }) {
8383
return <slots.lazy />;
8484
},
85-
})
86-
);
85+
}),
86+
});
8787

8888
const { container } = render(<Root />);
8989

@@ -144,14 +144,14 @@ describe("lazy", () => {
144144
});
145145
const slots = [lazyMachineSlot];
146146

147-
const Root = buildRootComponent(
148-
createXStateTreeMachine(rootMachine, {
147+
const Root = buildRootComponent({
148+
machine: createXStateTreeMachine(rootMachine, {
149149
slots,
150150
View({ slots }) {
151151
return <slots.lazy />;
152152
},
153-
})
154-
);
153+
}),
154+
});
155155

156156
const { container } = render(<Root />);
157157

src/routing/createRoute/createRoute.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ import { parse, ParsedQuery, stringify } from "query-string";
33
import * as Z from "zod";
44

55
import { XstateTreeHistory } from "../../types";
6-
import { type IsEmptyObject } from "../../utils";
6+
import {
7+
type IsEmptyObject,
8+
type MarkOptionalLikePropertiesOptional,
9+
} from "../../utils";
710
import { joinRoutes } from "../joinRoutes";
811

9-
type EmptyKeys<T> = keyof {
10-
[K in keyof T as IsEmptyObject<T[K], true> extends true ? K : never]: T[K];
11-
};
12-
type MakeEmptyObjectPropertiesOptional<T> = Omit<T, EmptyKeys<T>> &
13-
Partial<Pick<T, EmptyKeys<T>>>;
14-
1512
/**
1613
* @public
1714
*/
@@ -67,10 +64,10 @@ export type RouteArgumentFunctions<
6764
? (args?: TArgs) => TReturn
6865
: EmptyRouteArguments<TParams, TQuery> extends true
6966
? (args?: Partial<TArgs>) => TReturn
70-
: (args: MakeEmptyObjectPropertiesOptional<TArgs>) => TReturn;
67+
: (args: MarkOptionalLikePropertiesOptional<TArgs>) => TReturn;
7168

7269
type RouteRedirect<TParams, TQuery, TMeta> = (
73-
args: MakeEmptyObjectPropertiesOptional<{
70+
args: MarkOptionalLikePropertiesOptional<{
7471
params: TParams;
7572
query: TQuery;
7673
meta?: TMeta;

src/test-app/AppMachine.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,13 @@ export const BuiltAppMachine = createXStateTreeMachine(AppMachine, {
9696
},
9797
});
9898

99-
export const App = buildRootComponent(BuiltAppMachine, {
100-
history,
101-
basePath: "",
102-
routes: [homeRoute, settingsRoute],
103-
getPathName: () => "/",
104-
getQueryString: () => "",
99+
export const App = buildRootComponent({
100+
machine: BuiltAppMachine,
101+
routing: {
102+
history,
103+
basePath: "",
104+
routes: [homeRoute, settingsRoute],
105+
getPathName: () => "/",
106+
getQueryString: () => "",
107+
},
105108
});

src/test-app/tests/itWorksWithoutRouting.integration.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const root = createXStateTreeMachine(rootMachine, {
4343
},
4444
});
4545

46-
const RootView = buildRootComponent(root);
46+
const RootView = buildRootComponent({ machine: root });
4747

4848
describe("Environment without routing", () => {
4949
it("still works without error", () => {

src/test-app/tests/selectorsStaleCanHandleEvent.integration.tsx

+9-6
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import { OtherMachine } from "../OtherMachine";
88
import { settingsRoute } from "../routes";
99

1010
const history = createMemoryHistory<any>();
11-
const App = buildRootComponent(OtherMachine, {
12-
history,
13-
basePath: "",
14-
routes: [settingsRoute],
15-
getPathName: () => "/settings",
16-
getQueryString: () => "",
11+
const App = buildRootComponent({
12+
machine: OtherMachine,
13+
routing: {
14+
history,
15+
basePath: "",
16+
routes: [settingsRoute],
17+
getPathName: () => "/settings",
18+
getQueryString: () => "",
19+
},
1720
});
1821

1922
describe("Selectors & canHandleEvent", () => {

src/tests/actionsGetUpdatedSelectors.spec.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ describe("actions accessing selectors", () => {
2828
},
2929
});
3030

31-
const Root = buildRootComponent(
32-
createXStateTreeMachine(machine, {
31+
const Root = buildRootComponent({
32+
machine: createXStateTreeMachine(machine, {
3333
actions({ selectors, send }) {
3434
actionsCallCount++;
3535
return {
@@ -43,8 +43,8 @@ describe("actions accessing selectors", () => {
4343
<button onClick={actions.incrementCount}>{selectors.count}</button>
4444
);
4545
},
46-
})
47-
);
46+
}),
47+
});
4848

4949
it("gets the most up to date selectors value without re-creating the action functions", async () => {
5050
const { getByRole, rerender } = render(<Root />);

src/tests/asyncRouteRedirects.spec.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,16 @@ describe("async route redirects", () => {
7070
},
7171
});
7272

73-
const Root = buildRootComponent(
74-
createXStateTreeMachine(machine, {
73+
const Root = buildRootComponent({
74+
machine: createXStateTreeMachine(machine, {
7575
View: ({ selectors }) => <p>{selectors.bar}</p>,
7676
}),
77-
{
77+
routing: {
7878
basePath: "/",
7979
history: hist,
8080
routes: [parentRoute, redirectRoute, childRoute],
81-
}
82-
);
81+
},
82+
});
8383

8484
it("handles a top/middle/bottom route hierarchy where top and middle perform a redirect", async () => {
8585
const { queryByText } = render(<Root />);

src/utils.ts

+12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ export type OmitOptional<T> = {
1313
? P
1414
: never]: T[P];
1515
};
16+
17+
export type EmptyKeys<T> = keyof {
18+
[K in keyof T as IsEmptyObject<T[K], true> extends true ? K : never]: T[K];
19+
};
20+
21+
/**
22+
* Marks any required property that can accept undefined as optional
23+
*/
24+
export type MarkOptionalLikePropertiesOptional<T> = Omit<T, EmptyKeys<T>> &
25+
Partial<Pick<T, EmptyKeys<T>>>;
26+
1627
export type IsEmptyObject<
1728
Obj,
1829
ExcludeOptional extends boolean = false
@@ -24,6 +35,7 @@ export type IsEmptyObject<
2435
? true
2536
: false;
2637

38+
export type IsUnknown<T> = unknown extends T ? true : false;
2739
export function assertIsDefined<T>(
2840
val: T,
2941
msg?: string

0 commit comments

Comments
 (0)