Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
14 changes: 14 additions & 0 deletions .changeset/fep-2357-prepare-outside-react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@stackflow/react": minor
---

Expose `prepare` on the `stackflow()` output to preload an activity's component chunk and data loader from outside the React render tree (e.g. at app bootstrap, before the first render), without depending on React Context.

```ts
const { Stack, actions, stepActions, prepare } = stackflow({ config, components, plugins });

prepare("Article", { articleId: "123" }); // warm chunk + fire data loader
prepare("Article"); // warm chunk only
```

The signature matches the existing `usePrepare` hook (omitting params warms the chunk only; passing params also fires the loader), and `usePrepare` is now a thin wrapper over the same implementation, so in-tree callers are unchanged. Failures are delivered as a rejection of the returned promise rather than a synchronous throw.
9 changes: 9 additions & 0 deletions integrations/react/src/Prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {
InferActivityParams,
RegisteredActivityName,
} from "@stackflow/config";

export type Prepare = <K extends RegisteredActivityName>(
activityName: K,
activityParams?: InferActivityParams<K>,
) => Promise<void>;
1 change: 1 addition & 0 deletions integrations/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./Actions";
export * from "./ActivityComponentType";
export * from "./lazy";
export * from "./loader/useLoaderData";
export * from "./Prepare";
export * from "./StackComponentType";
export * from "./StaticActivityComponentType";
export * from "./StepActions";
Expand Down
62 changes: 62 additions & 0 deletions integrations/react/src/makePrepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type {
ActivityDefinition,
Config,
InferActivityParams,
RegisteredActivityName,
} from "@stackflow/config";
import type { ActivityComponentType } from "./BaseActivityComponentType";
import type { Prepare } from "./Prepare";
import {
getContentComponent,
isStructuredActivityComponent,
} from "./StructuredActivityComponentType";

export type MakePrepareInput = {
config: Config<ActivityDefinition<RegisteredActivityName>>;
loadData: (activityName: string, activityParams: {}) => unknown;
activityComponentMap: {
[activityName in RegisteredActivityName]: ActivityComponentType;
};
};

export function makePrepare({
config,
loadData,
activityComponentMap,
}: MakePrepareInput): Prepare {
return async function prepare<K extends RegisteredActivityName>(
activityName: K,
activityParams?: InferActivityParams<K>,
) {
const activityConfig = config.activities.find(
({ name }) => name === activityName,
);
const prefetchTasks: Promise<unknown>[] = [];

if (!activityConfig)
throw new Error(`Activity ${activityName} is not registered.`);

if (activityParams && activityConfig.loader) {
prefetchTasks.push(
Promise.resolve(loadData(activityName, activityParams)),
);
}

if ("_load" in activityComponentMap[activityName]) {
prefetchTasks.push(
Promise.resolve(activityComponentMap[activityName]._load?.()),
);
}

if (
isStructuredActivityComponent(activityComponentMap[activityName]) &&
typeof activityComponentMap[activityName].content === "function"
) {
prefetchTasks.push(
getContentComponent(activityComponentMap[activityName]).preload(),
);
}

await Promise.all(prefetchTasks);
};
}
8 changes: 8 additions & 0 deletions integrations/react/src/stackflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import type { Actions } from "./Actions";
import { ConfigProvider } from "./ConfigProvider";
import { DataLoaderProvider, loaderPlugin } from "./loader";
import { makeActions } from "./makeActions";
import { makePrepare } from "./makePrepare";
import { makeStepActions } from "./makeStepActions";
import type { StackComponentType } from "./StackComponentType";
import type { StepActions } from "./StepActions";
import type { Prepare } from "./Prepare";

export type StackflowPluginsEntry =
| StackflowReactPlugin<never>
Expand All @@ -47,6 +49,7 @@ export type StackflowOutput = {
Stack: StackComponentType;
actions: Actions;
stepActions: StepActions<ActivityBaseParams>;
prepare: Prepare;
};

export function stackflow<
Expand Down Expand Up @@ -201,5 +204,10 @@ export function stackflow<
Stack,
actions: makeActions(() => getCoreStore()?.actions),
stepActions: makeStepActions(() => getCoreStore()?.actions),
prepare: makePrepare({
config: input.config,
loadData,
activityComponentMap: input.components,
}),
};
}
62 changes: 12 additions & 50 deletions integrations/react/src/usePrepare.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,24 @@
import type {
InferActivityParams,
RegisteredActivityName,
} from "@stackflow/config";
import { useCallback } from "react";
import { useMemo } from "react";
import { useActivityComponentMap } from "./ActivityComponentMapProvider";
import {
getContentComponent,
isStructuredActivityComponent,
} from "./StructuredActivityComponentType";
import { useDataLoader } from "./loader";
import { makePrepare } from "./makePrepare";
import type { Prepare } from "./Prepare";
import { useConfig } from "./useConfig";

export type Prepare = <K extends RegisteredActivityName>(
activityName: K,
activityParams?: InferActivityParams<K>,
) => Promise<void>;

/**
* `stackflow()` 출력의 `prepare`와 동일 로직을 감싸는 얇은 래퍼.
*
* React Context에서 파생되는 세 입력(`config`, `loadData`, `activityComponentMap`)을
* `makePrepare`에 그대로 넘긴다. 셋 중 하나라도 바뀌지 않는 한 반환 함수의 참조가
* 안정적이도록 메모이즈한다.
*/
export function usePrepare(): Prepare {
const config = useConfig();
const loadData = useDataLoader();
const activityComponentMap = useActivityComponentMap();

return useCallback(
async function prepare<K extends RegisteredActivityName>(
activityName: K,
activityParams?: InferActivityParams<K>,
) {
const activityConfig = config.activities.find(
({ name }) => name === activityName,
);
const prefetchTasks: Promise<unknown>[] = [];

if (!activityConfig)
throw new Error(`Activity ${activityName} is not registered.`);

if (activityParams && activityConfig.loader) {
prefetchTasks.push(
Promise.resolve(loadData(activityName, activityParams)),
);
}

if ("_load" in activityComponentMap[activityName]) {
prefetchTasks.push(
Promise.resolve(activityComponentMap[activityName]._load?.()),
);
}

if (
isStructuredActivityComponent(activityComponentMap[activityName]) &&
typeof activityComponentMap[activityName].content === "function"
) {
prefetchTasks.push(
getContentComponent(activityComponentMap[activityName]).preload(),
);
}

await Promise.all(prefetchTasks);
},
return useMemo(
() => makePrepare({ config, loadData, activityComponentMap }),
[config, loadData, activityComponentMap],
);
}
Loading