Skip to content

Commit d906597

Browse files
feat(react/future): Add prefetch API for lazy activity component and loader data. (#616)
1 parent 8ee8c8f commit d906597

File tree

17 files changed

+314
-105
lines changed

17 files changed

+314
-105
lines changed

.changeset/slick-heads-tan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@stackflow/react": minor
3+
---
4+
5+
Add prefetch API for lazy activity component and loader data.
6+
- A hook `usePrepare()` which returns `prepare(activityName[, activityParams])` is added for navigation warmup.
7+
- A hook `useActivityPreparation(activities)` for preparing navigations inside a component is added.

config/src/ActivityLoader.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import type { ActivityLoaderArgs } from "./ActivityLoaderArgs";
22
import type { RegisteredActivityName } from "./RegisteredActivityName";
33

4-
export type ActivityLoader<ActivityName extends RegisteredActivityName> = (
5-
args: ActivityLoaderArgs<ActivityName>,
6-
) => any;
4+
export type ActivityLoader<ActivityName extends RegisteredActivityName> = {
5+
(args: ActivityLoaderArgs<ActivityName>): any;
6+
loaderCacheMaxAge?: number;
7+
};
8+
9+
export function loader<ActivityName extends RegisteredActivityName>(
10+
loaderFn: (args: ActivityLoaderArgs<ActivityName>) => any,
11+
options?: {
12+
loaderCacheMaxAge?: number;
13+
},
14+
): ActivityLoader<ActivityName> {
15+
return Object.assign(
16+
(args: ActivityLoaderArgs<ActivityName>) => loaderFn(args),
17+
options,
18+
);
19+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { RegisteredActivityName } from "@stackflow/config";
2+
import type { PropsWithChildren } from "react";
3+
import { createContext, useContext } from "react";
4+
import type { ActivityComponentType } from "./ActivityComponentType";
5+
6+
const ActivityComponentMapContext = createContext<
7+
| {
8+
[activityName in RegisteredActivityName]: ActivityComponentType;
9+
}
10+
| null
11+
>(null);
12+
13+
export function useActivityComponentMap() {
14+
const context = useContext(ActivityComponentMapContext);
15+
if (context === null) {
16+
throw new Error(
17+
"useActivityComponentMap must be used within ActivityComponentMapProvider",
18+
);
19+
}
20+
return context;
21+
}
22+
23+
type ActivityComponentMapProviderProps = PropsWithChildren<{
24+
value: {
25+
[activityName in RegisteredActivityName]: ActivityComponentType;
26+
};
27+
}>;
28+
29+
export function ActivityComponentMapProvider({
30+
children,
31+
value,
32+
}: ActivityComponentMapProviderProps) {
33+
return (
34+
<ActivityComponentMapContext.Provider value={value}>
35+
{children}
36+
</ActivityComponentMapContext.Provider>
37+
);
38+
}

integrations/react/src/__internal__/MainRenderer.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,9 @@ import { StackProvider } from "./stack";
77
import type { WithRequired } from "./utils";
88

99
interface MainRendererProps {
10-
activityComponentMap: {
11-
[key: string]: ActivityComponentType;
12-
};
1310
initialContext: any;
1411
}
15-
const MainRenderer: React.FC<MainRendererProps> = ({
16-
activityComponentMap,
17-
initialContext,
18-
}) => {
12+
const MainRenderer: React.FC<MainRendererProps> = ({ initialContext }) => {
1913
const coreState = useCoreState();
2014
const plugins = usePlugins();
2115

@@ -40,7 +34,6 @@ const MainRenderer: React.FC<MainRendererProps> = ({
4034
{renderingPlugins.map((plugin) => (
4135
<PluginRenderer
4236
key={plugin.key}
43-
activityComponentMap={activityComponentMap}
4437
plugin={plugin}
4538
initialContext={initialContext}
4639
/>

integrations/react/src/__internal__/PluginRenderer.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
import type React from "react";
2-
import type { ActivityComponentType } from "./ActivityComponentType";
2+
import { useActivityComponentMap } from "./ActivityComponentMapProvider";
33
import { ActivityProvider } from "./activity";
44
import { useCoreState } from "./core";
55
import { usePlugins } from "./plugins";
66
import type { StackflowReactPlugin } from "./StackflowReactPlugin";
77
import type { WithRequired } from "./utils";
88

99
interface PluginRendererProps {
10-
activityComponentMap: {
11-
[key: string]: ActivityComponentType;
12-
};
1310
plugin: WithRequired<ReturnType<StackflowReactPlugin>, "render">;
1411
initialContext: any;
1512
}
1613
const PluginRenderer: React.FC<PluginRendererProps> = ({
17-
activityComponentMap,
1814
plugin,
1915
initialContext,
2016
}) => {
17+
const activityComponentMap = useActivityComponentMap();
2118
const coreState = useCoreState();
2219
const plugins = usePlugins();
2320

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
2+
return (
3+
typeof value === "object" &&
4+
value !== null &&
5+
"then" in value &&
6+
typeof value.then === "function"
7+
);
8+
}
Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
1-
/**
2-
* Main
3-
*/
4-
51
export * from "../__internal__/activity/useActivity";
6-
7-
/**
8-
* Types
9-
*/
102
export * from "../__internal__/StackflowReactPlugin";
11-
/**
12-
* Hooks
13-
*/
143
export * from "../__internal__/stack/useStack";
154
export * from "./Actions";
165
export * from "./ActivityComponentType";
17-
/**
18-
* Utils
19-
*/
206
export * from "./lazy";
217
export * from "./loader/useLoaderData";
228
export * from "./StackComponentType";
239
export * from "./StepActions";
2410
export * from "./stackflow";
2511
export * from "./useActivityParams";
12+
export * from "./useActivityPreparation";
2613
export * from "./useConfig";
2714
export * from "./useFlow";
15+
export * from "./usePrepare";
2816
export * from "./useStepFlow";

integrations/react/src/future/lazy.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import type { StaticActivityComponentType } from "../__internal__/StaticActivity
55
export function lazy<T extends { [K in keyof T]: any } = {}>(
66
load: () => Promise<{ default: StaticActivityComponentType<T> }>,
77
): LazyActivityComponentType<T> {
8-
let cachedValue: { default: StaticActivityComponentType<T> } | null = null;
8+
let cachedValue: Promise<{ default: StaticActivityComponentType<T> }> | null =
9+
null;
910

10-
const cachedLoad = async () => {
11+
const cachedLoad = () => {
1112
if (!cachedValue) {
12-
const value = await load();
13-
cachedValue = value;
13+
cachedValue = load();
14+
cachedValue.catch((error) => {
15+
cachedValue = null;
16+
17+
throw error;
18+
});
1419
}
1520
return cachedValue;
1621
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createContext, type ReactNode, useContext } from "react";
2+
3+
export const DataLoaderContext = createContext<
4+
((activityName: string, activityParams: {}) => unknown) | null
5+
>(null);
6+
7+
export function DataLoaderProvider({
8+
loadData,
9+
children,
10+
}: {
11+
loadData: (activityName: string, activityParams: {}) => unknown;
12+
children: ReactNode;
13+
}) {
14+
return (
15+
<DataLoaderContext.Provider value={loadData}>
16+
{children}
17+
</DataLoaderContext.Provider>
18+
);
19+
}
20+
21+
export function useDataLoader() {
22+
const loadData = useContext(DataLoaderContext);
23+
24+
if (!loadData) {
25+
throw new Error("useDataLoader() must be used within a DataLoaderProvider");
26+
}
27+
28+
return loadData;
29+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from "./DataLoaderContext";
12
export * from "./loaderPlugin";
23
export * from "./useLoaderData";

0 commit comments

Comments
 (0)