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

useFlowState Hook

The useFlowState hook provides access to the current flow state, context, and navigation methods from within step components. It can be used in two ways: with a flow-specific hook or the generic hook.

Using the flow’s custom hook provides full type safety:

import { defineFlow } from "@useflow/react";
const myFlow = defineFlow({
id: "myFlow",
start: "step1",
steps: {
step1: { next: "step2" },
step2: { next: "step3" },
step3: {}
}
});
function MyStep() {
const { stepId, next, context } = myFlow.useFlowState({ step: "step1" });
// All properties are fully typed for this specific flow
return <div>{stepId}</div>;
}

For accessing flow state in shared components:

import { useFlowState } from "@useflow/react";
function SharedStepComponent() {
const { stepId, context, next } = useFlowState<MyContextType>();
// Generic hook - requires context type parameter
return <div>{stepId}</div>;
}

The hook returns an object containing:

{
// Current step
stepId: string;
step: StepInfo; // Current step configuration
// Flow context
context: TContext;
// Flow status
status: "active" | "completed";
// Navigation availability
canGoBack: boolean; // True if user can navigate back
canGoNext: boolean; // True if user can navigate forward
// Timestamps
startedAt: number;
completedAt?: number;
// Navigation tracking
path: readonly PathEntry[];
history: readonly HistoryEntry[];
}
{
// Move forward
next: (target?: string, update?: ContextUpdate) => void;
next: (update?: ContextUpdate) => void;
// Skip step
skip: (target?: string, update?: ContextUpdate) => void;
skip: (update?: ContextUpdate) => void;
// Go back
back: () => void;
// Reset flow
reset: () => void;
// Update context
setContext: (update: ContextUpdate<TContext>) => void;
// Restore saved state
restore: (state: FlowState) => void;
}
{
// All steps in the flow
steps: Record<string, StepInfo>;
// Possible next steps from current position
nextSteps?: readonly string[];
// Persistence state
isRestoring: boolean;
// Save state (when using persistence)
save: () => Promise<void>;
}
interface PathEntry {
stepId: string;
startedAt: number;
}
interface HistoryEntry {
stepId: string;
action: "next" | "skip" | "back";
startedAt: number;
completedAt?: number;
}
type ContextUpdate<TContext> =
| Partial<TContext> // Object with partial updates
| ((current: TContext) => TContext); // Updater function
interface StepInfo<TNext = string> {
next?: TNext | readonly TNext[];
}
function ProfileStep() {
const { next, back, context } = myFlow.useFlowState({ step: "profile" });
return (
<div>
<h2>Welcome, {context.name}!</h2>
<button onClick={back}>Back</button>
<button onClick={() => next()}>Continue</button>
</div>
);
}
function FormStep() {
const { next, setContext } = myFlow.useFlowState({ step: "form" });
const [name, setName] = useState("");
const handleSubmit = () => {
// Update context and navigate
next({ name });
// Or update context separately
setContext({ name });
next();
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
function BranchStep() {
const { next, context } = myFlow.useFlowState({ step: "branch" });
return (
<div>
{/* Component-driven navigation */}
<button onClick={() => next("option1")}>
Choose Option 1
</button>
<button onClick={() => next("option2")}>
Choose Option 2
</button>
{/* Or context-driven (with resolver) */}
<button onClick={() => next({ userType: "business" })}>
Continue as Business
</button>
</div>
);
}
function OptionalStep() {
const { next, skip } = myFlow.useFlowState({ step: "optional" });
return (
<div>
<h2>Optional Configuration</h2>
<button onClick={() => next({ configured: true })}>
Configure
</button>
<button onClick={() => skip({ skipped: true })}>
Skip for now
</button>
</div>
);
}
function NavigableStep() {
const { next, back, canGoBack, canGoNext } = myFlow.useFlowState({ step: "middle" });
return (
<div>
<button onClick={back} disabled={!canGoBack}>
Back
</button>
<button onClick={() => next()} disabled={!canGoNext}>
Next
</button>
</div>
);
}
function CurrentStep() {
const { step, stepId, steps } = myFlow.useFlowState({ step: "current" });
// Current step config
console.log("Current step:", stepId);
console.log("Next steps:", step.next);
// All steps in flow
console.log("Total steps:", Object.keys(steps).length);
return <div>Step {stepId}</div>;
}
function ProgressStep() {
const { path, history, startedAt } = myFlow.useFlowState({ step: "progress" });
// Linear progress
const stepsCompleted = path.length - 1;
// Total actions taken (including back navigation)
const totalActions = history.length;
// Time elapsed
const timeElapsed = Date.now() - startedAt;
return (
<div>
<p>Steps completed: {stepsCompleted}</p>
<p>Total actions: {totalActions}</p>
<p>Time: {Math.floor(timeElapsed / 1000)}s</p>
</div>
);
}
function PersistableStep() {
const { save, isRestoring } = myFlow.useFlowState({ step: "persistable" });
if (isRestoring) {
return <div>Loading saved progress...</div>;
}
const handleSave = async () => {
await save();
alert("Progress saved!");
};
return (
<div>
<button onClick={handleSave}>Save Progress</button>
</div>
);
}

Always prefer the flow-specific hook for better type safety:

// ✅ Good - full type safety
const { next, context, stepId } = myFlow.useFlowState({ step: "myStep" });
// ❌ Avoid - requires manual typing
const { next, context, stepId } = useFlowState<MyContext>();

Check isRestoring when using persistence:

function MyStep() {
const { isRestoring } = myFlow.useFlowState({ step: "myStep" });
if (isRestoring) {
return <LoadingSpinner />;
}
return <div>Ready!</div>;
}

Always validate data before navigating:

function FormStep() {
const { next } = myFlow.useFlowState({ step: "form" });
const [data, setData] = useState({});
const isValid = validateData(data);
return (
<button
onClick={() => next(data)}
disabled={!isValid}
>
Continue
</button>
);
}

Choose the right navigation method:

// ✅ Good - semantic actions
next({ data }); // User completed step
skip({ skipped: true }); // User chose to skip
back(); // User went back
// ❌ Bad - unclear intent
next({ skipped: true }); // Confusing - is this a skip?