Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c447299
test
ENvironmentSet Jul 31, 2025
479fa84
optimize
ENvironmentSet Jul 31, 2025
a1d42ba
fix
ENvironmentSet Jul 31, 2025
d97b03e
fix
ENvironmentSet Jul 31, 2025
833d47d
format
ENvironmentSet Jul 31, 2025
6ec8c73
format
ENvironmentSet Jul 31, 2025
e52974e
fix
ENvironmentSet Jul 31, 2025
2658640
refactor
ENvironmentSet Jul 31, 2025
cb1ab37
refactor: reduce complexivity
Aug 10, 2025
6a8333b
fix: type error
Aug 10, 2025
9f952f7
feat: Detach prepare logic from useFlow
Aug 10, 2025
31c2f62
add exports
Aug 10, 2025
8530765
changeset
Aug 10, 2025
53e5454
chore: run format
orionmiz Aug 11, 2025
3926564
Update changeset
Aug 13, 2025
e6a009a
Handle thenable
Aug 13, 2025
e2aed6f
optimize cache gc
Aug 13, 2025
a8dcadf
Optimize cache entry creation
Aug 13, 2025
792a17b
use activityComponentMapContext
Aug 13, 2025
5545142
do not cache rejected promise
Aug 13, 2025
978ba32
accept thenable
Aug 13, 2025
410b4b9
fix replace id bug
Aug 13, 2025
74ce220
fix import
Aug 13, 2025
dfd5291
fix import
Aug 13, 2025
ece2e9d
separate prepare from actions
Aug 13, 2025
2dee06d
update changeset
Aug 13, 2025
9ceb57f
spec compat
Aug 13, 2025
85868a3
use promiselike
Aug 13, 2025
9b2a4b5
revert
Aug 13, 2025
322f64d
run format
Aug 13, 2025
29b613b
don't cache rejected promise
Aug 13, 2025
19be7f7
throw error
Aug 13, 2025
b155672
Merge branch 'main' into route-preload
ENvironmentSet Aug 18, 2025
cbd7cfd
remove unused import
Aug 18, 2025
24c1600
add loader constructor
Aug 18, 2025
1ef17c7
fix
Aug 18, 2025
55f23c8
update
Aug 18, 2025
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
7 changes: 7 additions & 0 deletions .changeset/slick-heads-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@stackflow/react": minor
---

Add prefetch API for lazy activity component and loader data.
- A hook `usePrepare()` which returns `prepare(activityName[, activityParams])` is added for navigation warmup.
- A hook `useActivityPreparation(activityNames)` for preparing navigations inside a component is added.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { RegisteredActivityName } from "@stackflow/config";
import type { PropsWithChildren } from "react";
import type { ActivityComponentType } from "./ActivityComponentType";
import { createContext, useContext } from "react";

const ActivityComponentMapContext = createContext<
| {
[activityName in RegisteredActivityName]: ActivityComponentType;
}
| null
>(null);

export function useActivityComponentMap() {
const context = useContext(ActivityComponentMapContext);
if (context === null) {
throw new Error(
"useActivityComponentMap must be used within ActivityComponentMapProvider",
);
}
return context;
}

type ActivityComponentMapProviderProps = PropsWithChildren<{
value: {
[activityName in RegisteredActivityName]: ActivityComponentType;
};
}>;

export function ActivityComponentMapProvider({
children,
value,
}: ActivityComponentMapProviderProps) {
return (
<ActivityComponentMapContext.Provider value={value}>
{children}
</ActivityComponentMapContext.Provider>
);
}
9 changes: 1 addition & 8 deletions integrations/react/src/__internal__/MainRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ import { StackProvider } from "./stack";
import type { WithRequired } from "./utils";

interface MainRendererProps {
activityComponentMap: {
[key: string]: ActivityComponentType;
};
initialContext: any;
}
const MainRenderer: React.FC<MainRendererProps> = ({
activityComponentMap,
initialContext,
}) => {
const MainRenderer: React.FC<MainRendererProps> = ({ initialContext }) => {
const coreState = useCoreState();
const plugins = usePlugins();

Expand All @@ -40,7 +34,6 @@ const MainRenderer: React.FC<MainRendererProps> = ({
{renderingPlugins.map((plugin) => (
<PluginRenderer
key={plugin.key}
activityComponentMap={activityComponentMap}
plugin={plugin}
initialContext={initialContext}
/>
Expand Down
111 changes: 54 additions & 57 deletions integrations/react/src/__internal__/PluginRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,72 @@
import type React from "react";
import type { ActivityComponentType } from "./ActivityComponentType";
import { useActivityComponentMap } from "./ActivityComponentMapProvider";
import type { StackflowReactPlugin } from "./StackflowReactPlugin";
import { ActivityProvider } from "./activity";
import { useCoreState } from "./core";
import { usePlugins } from "./plugins";
import type { StackflowReactPlugin } from "./StackflowReactPlugin";
import type { WithRequired } from "./utils";

interface PluginRendererProps {
activityComponentMap: {
[key: string]: ActivityComponentType;
};
plugin: WithRequired<ReturnType<StackflowReactPlugin>, "render">;
initialContext: any;
plugin: WithRequired<ReturnType<StackflowReactPlugin>, "render">;
initialContext: any;
}
const PluginRenderer: React.FC<PluginRendererProps> = ({
activityComponentMap,
plugin,
initialContext,
plugin,
initialContext,
}) => {
const coreState = useCoreState();
const plugins = usePlugins();
const activityComponentMap = useActivityComponentMap();
const coreState = useCoreState();
const plugins = usePlugins();

return plugin.render({
stack: {
...coreState,
render(overrideStack) {
const stack = {
...coreState,
...overrideStack,
};
return plugin.render({
stack: {
...coreState,
render(overrideStack) {
const stack = {
...coreState,
...overrideStack,
};

return {
activities: stack.activities.map((activity) => ({
...activity,
key: activity.id,
render(overrideActivity) {
const Activity = activityComponentMap[activity.name];
return {
activities: stack.activities.map((activity) => ({
...activity,
key: activity.id,
render(overrideActivity) {
const Activity = activityComponentMap[activity.name];

let output: React.ReactNode = (
<Activity params={activity.params} />
);
let output: React.ReactNode = (
<Activity params={activity.params} />
);

plugins.forEach((p) => {
output =
p.wrapActivity?.({
activity: {
...activity,
render: () => output,
},
initialContext,
}) ?? output;
});
plugins.forEach((p) => {
output =
p.wrapActivity?.({
activity: {
...activity,
render: () => output,
},
initialContext,
}) ?? output;
});

return (
<ActivityProvider
key={activity.id}
value={{
...activity,
...overrideActivity,
}}
>
{output}
</ActivityProvider>
);
},
})),
};
},
},
initialContext,
});
return (
<ActivityProvider
key={activity.id}
value={{
...activity,
...overrideActivity,
}}
>
{output}
</ActivityProvider>
);
},
})),
};
},
},
initialContext,
});
};

PluginRenderer.displayName = "PluginRenderer";
Expand Down
15 changes: 15 additions & 0 deletions integrations/react/src/__internal__/utils/isThenable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Thenable<T> {
then<R>(
onFulfilled: (value: T) => R,
onRejected?: (reason: unknown) => R,
): Thenable<Awaited<R>>;
}

export function isThenable(value: unknown): value is Thenable<unknown> {
return (
typeof value === "object" &&
value !== null &&
"then" in value &&
typeof value.then === "function"
);
}
16 changes: 2 additions & 14 deletions integrations/react/src/future/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
/**
* Main
*/

export * from "../__internal__/activity/useActivity";

/**
* Types
*/
export * from "../__internal__/StackflowReactPlugin";
/**
* Hooks
*/
export * from "../__internal__/stack/useStack";
export * from "./Actions";
export * from "./ActivityComponentType";
/**
* Utils
*/
export * from "./lazy";
export * from "./loader/useLoaderData";
export * from "./StackComponentType";
export * from "./StepActions";
export * from "./stackflow";
export * from "./useActivityParams";
export * from "./useActivityPreparation";
export * from "./usePrepare";
export * from "./useConfig";
export * from "./useFlow";
export * from "./useStepFlow";
13 changes: 9 additions & 4 deletions integrations/react/src/future/lazy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import type { StaticActivityComponentType } from "../__internal__/StaticActivity
export function lazy<T extends { [K in keyof T]: any } = {}>(
load: () => Promise<{ default: StaticActivityComponentType<T> }>,
): LazyActivityComponentType<T> {
let cachedValue: { default: StaticActivityComponentType<T> } | null = null;
let cachedValue: Promise<{ default: StaticActivityComponentType<T> }> | null =
null;

const cachedLoad = async () => {
const cachedLoad = () => {
if (!cachedValue) {
const value = await load();
cachedValue = value;
cachedValue = load();
cachedValue.catch((error) => {
cachedValue = null;

throw error;
});
}
return cachedValue;
};
Expand Down
29 changes: 29 additions & 0 deletions integrations/react/src/future/loader/DataLoaderContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createContext, type ReactNode, useContext } from "react";

export const DataLoaderContext = createContext<
((activityName: string, activityParams: {}) => unknown) | null
>(null);

export function DataLoaderProvider({
loadData,
children,
}: {
loadData: (activityName: string, activityParams: {}) => unknown;
children: ReactNode;
}) {
return (
<DataLoaderContext.Provider value={loadData}>
{children}
</DataLoaderContext.Provider>
);
}

export function useDataLoader() {
const loadData = useContext(DataLoaderContext);

if (!loadData) {
throw new Error("useDataLoader() must be used within a DataLoaderProvider");
}

return loadData;
}
1 change: 1 addition & 0 deletions integrations/react/src/future/loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./DataLoaderContext";
export * from "./loaderPlugin";
export * from "./useLoaderData";
Loading