Skip to content
⚠️ Beta: API may change before v1.0. Pin to ~0.x.0 to avoid breaking changes.

<Flow> Component

The <Flow> component is the primary React component for rendering flows using the render props pattern.

import { Flow } from "@useflow/react";

The flow definition returned by defineFlow().

flow: RuntimeFlowDefinition<FlowDefinition, TContext>

Render function that receives flow state and returns React elements.

children: (state: FlowState) => ReactNode

Flow State Object:

{
// State
stepId: string;
step: StepInfo; // Current step configuration object
context: TContext;
status: "active" | "completed";
// Navigation
next: (target?: string, update?: ContextUpdate) => void;
skip: (target?: string, update?: ContextUpdate) => void;
back: () => void;
reset: () => void;
// Context Management
setContext: (update: ContextUpdate<TContext>) => void;
// Metadata
steps: Record<string, StepInfo>;
nextSteps: readonly string[] | undefined;
path: readonly PathEntry[];
history: readonly HistoryEntry[];
startedAt: number;
completedAt?: number;
// Persistence
save: () => Promise<void>;
restore: (state: FlowState) => void;
isRestoring: boolean;
// Rendering
renderStep: (elements: StepElements) => ReactElement;
}

Initial context state for the flow.

initialContext?: TContext

Default: {}

Unique identifier for flow instances (useful for reusable flows).

instanceId?: string

Persister for saving/restoring flow state.

persister?: FlowPersister

When to automatically save state.

saveMode?: "always" | "navigation" | "manual"
  • "navigation" (default) - Save on next/skip/back
  • "always" - Save on every context update
  • "manual" - Only save when calling save()

Debounce delay for save operations (milliseconds).

saveDebounce?: number

Default: 300

Component to show while restoring state.

loadingComponent?: ReactNode

Called when flow reaches a terminal step.

onComplete?: (event: { context: TContext }) => void

Called on forward navigation.

onNext?: (event: {
from: string;
to: string;
oldContext: TContext;
newContext: TContext;
}) => void

Called when skipping a step.

onSkip?: (event: {
from: string;
to: string;
oldContext: TContext;
newContext: TContext;
}) => void

Called on backward navigation.

onBack?: (event: {
from: string;
to: string;
oldContext: TContext;
newContext: TContext;
}) => void

Called on any navigation (next, skip, or back).

onTransition?: (event: {
from: string;
to: string;
direction: "forward" | "backward";
oldContext: TContext;
newContext: TContext;
}) => void

Called when context is updated.

onContextUpdate?: (event: {
oldContext: TContext;
newContext: TContext;
}) => void

Called after state is saved.

onSave?: (state: PersistedFlowState<TContext>) => void

Called after state is restored.

onRestore?: (state: PersistedFlowState<TContext>) => void

Called when save/restore fails.

onPersistenceError?: (error: Error) => void
<Flow flow={myFlow} initialContext={{ name: "" }}>
{({ renderStep }) =>
renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />,
complete: <CompleteStep />
})
}
</Flow>
<Flow flow={myFlow}>
{({ renderStep, stepId, steps }) => {
const stepKeys = Object.keys(steps);
const progress = ((stepKeys.indexOf(stepId) + 1) / stepKeys.length) * 100;
return (
<div>
<div className="progress-bar" style={{ width: `${progress}%` }} />
{renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />,
complete: <CompleteStep />
})}
</div>
);
}}
</Flow>
import { createLocalStorageStore, createPersister } from "@useflow/react";
const store = createLocalStorageStore(localStorage);
const persister = createPersister({ store });
<Flow
flow={myFlow}
persister={persister}
saveMode="navigation"
loadingComponent={<Spinner />}
>
{({ renderStep, isRestoring }) => {
if (isRestoring) return <Spinner />;
return renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />
});
}}
</Flow>
<Flow
flow={myFlow}
onNext={(event) => {
console.log(`Moving from ${event.from} to ${event.to}`);
analytics.track("step_forward", event);
}}
onComplete={(event) => {
console.log("Flow completed!", event.context);
submitData(event.context);
}}
>
{({ renderStep }) =>
renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />
})
}
</Flow>
<Flow flow={myFlow}>
{({ renderStep, stepId, context, back, next, canGoBack, canGoNext }) => (
<div className="custom-layout">
<header>
<h1>Step: {stepId}</h1>
</header>
<main>
{renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />,
complete: <CompleteStep />
})}
</main>
<footer>
<button onClick={back} disabled={!canGoBack}>
Back
</button>
<button onClick={() => next()} disabled={!canGoNext}>
Next
</button>
</footer>
</div>
)}
</Flow>
function TaskCreator({ taskId }: { taskId: string }) {
return (
<Flow
flow={taskFlow}
instanceId={taskId} // Separate state per task
persister={persister}
>
{({ renderStep }) =>
renderStep({
details: <DetailsStep />,
assign: <AssignStep />,
complete: <CompleteStep />
})
}
</Flow>
);
}
// Each instance has independent state
<TaskCreator taskId="task-1" />
<TaskCreator taskId="task-2" />

The renderStep function takes a map of step components and renders the current step:

renderStep: (elements: Record<string, ReactElement>) => ReactElement

Usage:

{({ renderStep }) =>
renderStep({
step1: <Step1Component />,
step2: <Step2Component />,
step3: <Step3Component />
})
}

Type Safety:

TypeScript ensures all steps are provided:

// ❌ Error: Missing step3
renderStep({
step1: <Step1 />,
step2: <Step2 />
})
// ✅ All steps provided
renderStep({
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />
})
// ✅ Good: Descriptive ID
<Flow instanceId={`order-${orderId}`} />
// ❌ Bad: Generic ID
<Flow instanceId="1" />
<Flow
flow={myFlow}
persister={persister}
loadingComponent={<Skeleton />}
>
{({ renderStep, isRestoring }) => (
isRestoring ? <Spinner /> : renderStep({ /* ... */ })
)}
</Flow>
const handleComplete = useCallback((event) => {
submitData(event.context);
}, []);
<Flow onComplete={handleComplete}>
// Set defaults in FlowProvider
<FlowProvider config={{ persister, saveMode: "navigation" }}>
{/* Override only when needed */}
<Flow flow={specialFlow} saveMode="always" />
</FlowProvider>