diff --git a/examples/todomvc/index.tsx b/examples/todomvc/index.tsx
index 4f5550a..30c1dc4 100644
--- a/examples/todomvc/index.tsx
+++ b/examples/todomvc/index.tsx
@@ -7,10 +7,13 @@ import { routes, history } from "./routes";
const appRoot = document.getElementById("root");
const root = createRoot(appRoot!);
-const App = buildRootComponent(TodoApp, {
- basePath: "/",
- history,
- routes,
+const App = buildRootComponent({
+ machine: TodoApp,
+ routing: {
+ basePath: "/",
+ history,
+ routes,
+ },
});
root.render();
diff --git a/src/builders.spec.tsx b/src/builders.spec.tsx
index 03b03c8..4a1ed14 100644
--- a/src/builders.spec.tsx
+++ b/src/builders.spec.tsx
@@ -11,7 +11,7 @@ describe("xstate-tree builders", () => {
describe("viewToMachine", () => {
it("takes a React view and wraps it in an xstate-tree machine that renders that view", async () => {
const ViewMachine = viewToMachine(() =>
hello world
);
- const Root = buildRootComponent(ViewMachine);
+ const Root = buildRootComponent({ machine: ViewMachine });
const { getByText } = render();
@@ -41,10 +41,13 @@ describe("xstate-tree builders", () => {
GO_TO_BAR: BarMachine,
});
- const Root = buildRootComponent(routingMachine, {
- history: hist,
- basePath: "/",
- routes: [fooRoute, barRoute],
+ const Root = buildRootComponent({
+ machine: routingMachine,
+ routing: {
+ history: hist,
+ basePath: "/",
+ routes: [fooRoute, barRoute],
+ },
});
const { getByText } = render();
diff --git a/src/lazy.spec.tsx b/src/lazy.spec.tsx
index e06fabc..15fc41d 100644
--- a/src/lazy.spec.tsx
+++ b/src/lazy.spec.tsx
@@ -19,7 +19,7 @@ describe("lazy", () => {
it("renders null by default when loading", () => {
const promiseFactory = () => new Promise(() => void 0);
const lazyMachine = lazy(promiseFactory);
- const Root = buildRootComponent(lazyMachine);
+ const Root = buildRootComponent({ machine: lazyMachine });
const { container, rerender } = render();
rerender();
@@ -32,7 +32,7 @@ describe("lazy", () => {
const lazyMachine = lazy(promiseFactory, {
Loader: () => loading
,
});
- const Root = buildRootComponent(lazyMachine);
+ const Root = buildRootComponent({ machine: lazyMachine });
const { container, rerender } = render();
rerender();
@@ -76,14 +76,14 @@ describe("lazy", () => {
});
const slots = [lazyMachineSlot];
- const Root = buildRootComponent(
- createXStateTreeMachine(rootMachine, {
+ const Root = buildRootComponent({
+ machine: createXStateTreeMachine(rootMachine, {
slots,
View({ slots }) {
return ;
},
- })
- );
+ }),
+ });
const { container } = render();
@@ -144,14 +144,14 @@ describe("lazy", () => {
});
const slots = [lazyMachineSlot];
- const Root = buildRootComponent(
- createXStateTreeMachine(rootMachine, {
+ const Root = buildRootComponent({
+ machine: createXStateTreeMachine(rootMachine, {
slots,
View({ slots }) {
return ;
},
- })
- );
+ }),
+ });
const { container } = render();
diff --git a/src/routing/createRoute/createRoute.ts b/src/routing/createRoute/createRoute.ts
index 36a9e11..b071ce0 100644
--- a/src/routing/createRoute/createRoute.ts
+++ b/src/routing/createRoute/createRoute.ts
@@ -3,15 +3,12 @@ import { parse, ParsedQuery, stringify } from "query-string";
import * as Z from "zod";
import { XstateTreeHistory } from "../../types";
-import { type IsEmptyObject } from "../../utils";
+import {
+ type IsEmptyObject,
+ type MarkOptionalLikePropertiesOptional,
+} from "../../utils";
import { joinRoutes } from "../joinRoutes";
-type EmptyKeys = keyof {
- [K in keyof T as IsEmptyObject extends true ? K : never]: T[K];
-};
-type MakeEmptyObjectPropertiesOptional = Omit> &
- Partial>>;
-
/**
* @public
*/
@@ -67,10 +64,10 @@ export type RouteArgumentFunctions<
? (args?: TArgs) => TReturn
: EmptyRouteArguments extends true
? (args?: Partial) => TReturn
- : (args: MakeEmptyObjectPropertiesOptional) => TReturn;
+ : (args: MarkOptionalLikePropertiesOptional) => TReturn;
type RouteRedirect = (
- args: MakeEmptyObjectPropertiesOptional<{
+ args: MarkOptionalLikePropertiesOptional<{
params: TParams;
query: TQuery;
meta?: TMeta;
diff --git a/src/test-app/AppMachine.tsx b/src/test-app/AppMachine.tsx
index e63dacb..9259d51 100644
--- a/src/test-app/AppMachine.tsx
+++ b/src/test-app/AppMachine.tsx
@@ -96,10 +96,13 @@ export const BuiltAppMachine = createXStateTreeMachine(AppMachine, {
},
});
-export const App = buildRootComponent(BuiltAppMachine, {
- history,
- basePath: "",
- routes: [homeRoute, settingsRoute],
- getPathName: () => "/",
- getQueryString: () => "",
+export const App = buildRootComponent({
+ machine: BuiltAppMachine,
+ routing: {
+ history,
+ basePath: "",
+ routes: [homeRoute, settingsRoute],
+ getPathName: () => "/",
+ getQueryString: () => "",
+ },
});
diff --git a/src/test-app/tests/itWorksWithoutRouting.integration.tsx b/src/test-app/tests/itWorksWithoutRouting.integration.tsx
index 460dccc..a7b53e9 100644
--- a/src/test-app/tests/itWorksWithoutRouting.integration.tsx
+++ b/src/test-app/tests/itWorksWithoutRouting.integration.tsx
@@ -43,7 +43,7 @@ const root = createXStateTreeMachine(rootMachine, {
},
});
-const RootView = buildRootComponent(root);
+const RootView = buildRootComponent({ machine: root });
describe("Environment without routing", () => {
it("still works without error", () => {
diff --git a/src/test-app/tests/selectorsStaleCanHandleEvent.integration.tsx b/src/test-app/tests/selectorsStaleCanHandleEvent.integration.tsx
index 9d1f146..68d7191 100644
--- a/src/test-app/tests/selectorsStaleCanHandleEvent.integration.tsx
+++ b/src/test-app/tests/selectorsStaleCanHandleEvent.integration.tsx
@@ -8,12 +8,15 @@ import { OtherMachine } from "../OtherMachine";
import { settingsRoute } from "../routes";
const history = createMemoryHistory();
-const App = buildRootComponent(OtherMachine, {
- history,
- basePath: "",
- routes: [settingsRoute],
- getPathName: () => "/settings",
- getQueryString: () => "",
+const App = buildRootComponent({
+ machine: OtherMachine,
+ routing: {
+ history,
+ basePath: "",
+ routes: [settingsRoute],
+ getPathName: () => "/settings",
+ getQueryString: () => "",
+ },
});
describe("Selectors & canHandleEvent", () => {
diff --git a/src/tests/actionsGetUpdatedSelectors.spec.tsx b/src/tests/actionsGetUpdatedSelectors.spec.tsx
index 26b0bec..4243aad 100644
--- a/src/tests/actionsGetUpdatedSelectors.spec.tsx
+++ b/src/tests/actionsGetUpdatedSelectors.spec.tsx
@@ -28,8 +28,8 @@ describe("actions accessing selectors", () => {
},
});
- const Root = buildRootComponent(
- createXStateTreeMachine(machine, {
+ const Root = buildRootComponent({
+ machine: createXStateTreeMachine(machine, {
actions({ selectors, send }) {
actionsCallCount++;
return {
@@ -43,8 +43,8 @@ describe("actions accessing selectors", () => {
);
},
- })
- );
+ }),
+ });
it("gets the most up to date selectors value without re-creating the action functions", async () => {
const { getByRole, rerender } = render();
diff --git a/src/tests/asyncRouteRedirects.spec.tsx b/src/tests/asyncRouteRedirects.spec.tsx
index 9908a53..f1d654c 100644
--- a/src/tests/asyncRouteRedirects.spec.tsx
+++ b/src/tests/asyncRouteRedirects.spec.tsx
@@ -70,16 +70,16 @@ describe("async route redirects", () => {
},
});
- const Root = buildRootComponent(
- createXStateTreeMachine(machine, {
+ const Root = buildRootComponent({
+ machine: createXStateTreeMachine(machine, {
View: ({ selectors }) => {selectors.bar}
,
}),
- {
+ routing: {
basePath: "/",
history: hist,
routes: [parentRoute, redirectRoute, childRoute],
- }
- );
+ },
+ });
it("handles a top/middle/bottom route hierarchy where top and middle perform a redirect", async () => {
const { queryByText } = render();
diff --git a/src/utils.ts b/src/utils.ts
index ea5512a..603c4ab 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -13,6 +13,17 @@ export type OmitOptional = {
? P
: never]: T[P];
};
+
+export type EmptyKeys = keyof {
+ [K in keyof T as IsEmptyObject extends true ? K : never]: T[K];
+};
+
+/**
+ * Marks any required property that can accept undefined as optional
+ */
+export type MarkOptionalLikePropertiesOptional = Omit> &
+ Partial>>;
+
export type IsEmptyObject<
Obj,
ExcludeOptional extends boolean = false
@@ -24,6 +35,7 @@ export type IsEmptyObject<
? true
: false;
+export type IsUnknown = unknown extends T ? true : false;
export function assertIsDefined(
val: T,
msg?: string
diff --git a/src/xstateTree.spec.tsx b/src/xstateTree.spec.tsx
index 0cda00f..7ab85c8 100644
--- a/src/xstateTree.spec.tsx
+++ b/src/xstateTree.spec.tsx
@@ -46,7 +46,7 @@ describe("xstate-tree", () => {
return Can swap: {selectors.canSwap}
;
},
});
- const Root = buildRootComponent(xstateTreeMachine);
+ const Root = buildRootComponent({ machine: xstateTreeMachine });
render();
await delay(10);
@@ -54,6 +54,35 @@ describe("xstate-tree", () => {
});
});
+ it("passes the supplied input through to the machine", async () => {
+ const machine = setup({
+ types: {
+ context: {} as { foo: string },
+ input: {} as { bar: string },
+ },
+ }).createMachine({
+ initial: "a",
+ context: ({ input }) => ({ foo: input.bar }),
+ states: {
+ a: {},
+ },
+ });
+
+ const XstateTreeMachine = createXStateTreeMachine(machine, {
+ View: ({ selectors }) => {
+ return {selectors.foo}
;
+ },
+ });
+ const Root = buildRootComponent({
+ machine: XstateTreeMachine,
+ input: { bar: "foo" },
+ });
+
+ const { getByText } = render();
+
+ getByText("foo");
+ });
+
describe("machines that don't have any visible change after initializing", () => {
it("still renders the machines view", async () => {
const renderCallback = jest.fn();
@@ -71,7 +100,7 @@ describe("xstate-tree", () => {
return null;
},
});
- const Root = buildRootComponent(XstateTreeMachine);
+ const Root = buildRootComponent({ machine: XstateTreeMachine });
render();
await delay(50);
@@ -102,7 +131,7 @@ describe("xstate-tree", () => {
return null;
},
});
- const Root = buildRootComponent(XstateTreeMachine);
+ const Root = buildRootComponent({ machine: XstateTreeMachine });
const { rerender } = render();
await delay(10);
@@ -138,7 +167,7 @@ describe("xstate-tree", () => {
const XstateTreeMachine = createXStateTreeMachine(machine, {
View: () => null,
});
- const Root = buildRootComponent(XstateTreeMachine);
+ const Root = buildRootComponent({ machine: XstateTreeMachine });
render();
@@ -195,7 +224,7 @@ describe("xstate-tree", () => {
const XstateTreeMachine = createXStateTreeMachine(machine, {
View: () => null,
});
- const Root = buildRootComponent(XstateTreeMachine);
+ const Root = buildRootComponent({ machine: XstateTreeMachine });
render();
@@ -226,7 +255,7 @@ describe("xstate-tree", () => {
return {selectors.foo}
;
},
});
- const Root = buildRootComponent(XstateTreeMachine);
+ const Root = buildRootComponent({ machine: XstateTreeMachine });
const { findByText } = render();
@@ -235,7 +264,7 @@ describe("xstate-tree", () => {
it("allows rendering nested roots", () => {
const childRoot = viewToMachine(() => Child
);
- const ChildRoot = buildRootComponent(childRoot);
+ const ChildRoot = buildRootComponent({ machine: childRoot });
const rootMachine = viewToMachine(() => {
return (
<>
@@ -244,7 +273,7 @@ describe("xstate-tree", () => {
>
);
});
- const Root = buildRootComponent(rootMachine);
+ const Root = buildRootComponent({ machine: rootMachine });
const { getByText } = render();
getByText("Root");
@@ -297,10 +326,13 @@ describe("xstate-tree", () => {
return I am root
;
},
});
- const Root = buildRootComponent(RootMachine, {
- basePath: "/",
- history: createMemoryHistory(),
- routes: [],
+ const Root = buildRootComponent({
+ machine: RootMachine,
+ routing: {
+ basePath: "/",
+ history: createMemoryHistory(),
+ routes: [],
+ },
});
const Root2Machine = createXStateTreeMachine(machine, {
@@ -308,10 +340,13 @@ describe("xstate-tree", () => {
return ;
},
});
- const Root2 = buildRootComponent(Root2Machine, {
- basePath: "/",
- history: createMemoryHistory(),
- routes: [],
+ const Root2 = buildRootComponent({
+ machine: Root2Machine,
+ routing: {
+ basePath: "/",
+ history: createMemoryHistory(),
+ routes: [],
+ },
});
try {
@@ -329,10 +364,10 @@ describe("xstate-tree", () => {
it("does not throw an error if either or one are a routing root", async () => {
const RootMachine = viewToMachine(() => I am root
);
- const Root = buildRootComponent(RootMachine);
+ const Root = buildRootComponent({ machine: RootMachine });
const Root2Machine = viewToMachine(() => );
- const Root2 = buildRootComponent(Root2Machine);
+ const Root2 = buildRootComponent({ machine: Root2Machine });
const { rerender } = render();
rerender();
diff --git a/src/xstateTree.tsx b/src/xstateTree.tsx
index a6ad4ef..65b5d70 100644
--- a/src/xstateTree.tsx
+++ b/src/xstateTree.tsx
@@ -14,6 +14,7 @@ import {
ActorRefFrom,
AnyEventObject,
AnyActorRef,
+ InputFrom,
} from "xstate";
import {
@@ -29,7 +30,13 @@ import { GetSlotNames, Slot } from "./slots";
import { GlobalEvents, AnyXstateTreeMachine, XstateTreeHistory } from "./types";
import { useConstant } from "./useConstant";
import { useService } from "./useService";
-import { assertIsDefined, mergeMeta, toJSON } from "./utils";
+import {
+ assertIsDefined,
+ mergeMeta,
+ toJSON,
+ type IsUnknown,
+ type MarkOptionalLikePropertiesOptional,
+} from "./utils";
export const emitter = new TinyEmitter();
@@ -269,6 +276,18 @@ export function recursivelySend(service: AnyActorRef, event: GlobalEvents) {
children.forEach((child) => recursivelySend(child, event));
}
+type RootOptions = {
+ routing:
+ | {
+ routes: AnyRoute[];
+ history: XstateTreeHistory;
+ basePath: string;
+ getPathName?: () => string;
+ getQueryString?: () => string;
+ }
+ | undefined;
+ input: IsUnknown extends true ? undefined : TInput;
+};
/**
* @public
*
@@ -277,16 +296,14 @@ export function recursivelySend(service: AnyActorRef, event: GlobalEvents) {
* @param machine - The root machine of the tree
* @param routing - The routing configuration for the tree
*/
-export function buildRootComponent(
- machine: AnyXstateTreeMachine,
- routing?: {
- routes: AnyRoute[];
- history: XstateTreeHistory;
- basePath: string;
- getPathName?: () => string;
- getQueryString?: () => string;
- }
+export function buildRootComponent(
+ options: { machine: TMachine } & MarkOptionalLikePropertiesOptional<
+ RootOptions>
+ >
) {
+ const { input, machine, routing } = options as unknown as {
+ machine: TMachine;
+ } & RootOptions>;
if (!machine._xstateTree) {
throw new Error(
"Root machine is not an xstate-tree machine, missing metadata"
@@ -299,6 +316,7 @@ export function buildRootComponent(
const RootComponent = function XstateTreeRootComponent() {
const lastSnapshotsRef = useRef>({});
const [_, __, interpreter] = useActor(machine, {
+ input,
inspect(event) {
switch (event.type) {
case "@xstate.actor":
diff --git a/xstate-tree.api.md b/xstate-tree.api.md
index 961ad9d..e081024 100644
--- a/xstate-tree.api.md
+++ b/xstate-tree.api.md
@@ -95,16 +95,15 @@ export function buildCreateRoute(history: () => XstateTreeHistory, basePath: str
}) => Route, ResolveZodType>, ResolveZodType, TEvent_1, MergeRouteTypes, TMeta_1> & SharedMeta>;
};
+// Warning: (ae-forgotten-export) The symbol "MarkOptionalLikePropertiesOptional" needs to be exported by the entry point index.d.ts
+// Warning: (ae-forgotten-export) The symbol "RootOptions" needs to be exported by the entry point index.d.ts
+//
// @public
-export function buildRootComponent(machine: AnyXstateTreeMachine, routing?: {
- routes: AnyRoute[];
- history: XstateTreeHistory;
- basePath: string;
- getPathName?: () => string;
- getQueryString?: () => string;
-}): {
+export function buildRootComponent(options: {
+ machine: TMachine;
+} & MarkOptionalLikePropertiesOptional>>): {
(): JSX.Element;
- rootMachine: AnyXstateTreeMachine;
+ rootMachine: TMachine;
};
// @public
@@ -243,10 +242,9 @@ export type Route = {
// Warning: (ae-forgotten-export) The symbol "IsEmptyObject" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "EmptyRouteArguments" needs to be exported by the entry point index.d.ts
-// Warning: (ae-forgotten-export) The symbol "MakeEmptyObjectPropertiesOptional" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
-export type RouteArgumentFunctions> = IsEmptyObject extends true ? () => TReturn : keyof TArgs extends "meta" ? (args?: TArgs) => TReturn : EmptyRouteArguments extends true ? (args?: Partial) => TReturn : (args: MakeEmptyObjectPropertiesOptional) => TReturn;
+export type RouteArgumentFunctions> = IsEmptyObject extends true ? () => TReturn : keyof TArgs extends "meta" ? (args?: TArgs) => TReturn : EmptyRouteArguments extends true ? (args?: Partial) => TReturn : (args: MarkOptionalLikePropertiesOptional) => TReturn;
// @public (undocumented)
export type RouteArguments = TParams extends undefined ? TQuery extends undefined ? TMeta extends undefined ? {} : {
@@ -399,9 +397,9 @@ export type XstateTreeMachineStateSchemaV2