Declarative
Define your flow in one place. Navigation logic lives in your flow config, not scattered across components.
Declarative
Define your flow in one place. Navigation logic lives in your flow config, not scattered across components.
Built-in Persistence
Two lines of code. Users resume exactly where they left off.
Type-Safe
Full TypeScript support. Autocomplete for step names, compile-time navigation errors.
Lightweight
Under 5KB gzipped. Zero dependencies. Just React.
Build a complete onboarding flow with conditional navigation in 3 simple steps:
import { defineFlow } from "@useflow/react";
type OnboardingContext = { email?: string; accountType?: "business" | "personal"; company?: string;};
const onboardingFlow = defineFlow({ id: "onboarding", start: "welcome", steps: { welcome: { next: "userType" }, userType: { next: ["business", "personal"] }, // 💡 Define all possible next steps business: { next: "complete" }, personal: { next: "complete" }, complete: {} } // 💡 Set the context type}).with<OnboardingContext>((steps) => ({ resolvers: { // 💡 Type-safe: can only return steps in next array userType: (ctx) => ctx.accountType === "business" ? steps.business // ✅ Valid : steps.personal // ✅ Valid // steps.complete would be a TypeScript error ❌ }}));import { onboardingFlow } from "./flow";
function UserTypeStep() { const { context, setContext, next } = onboardingFlow.useFlowState({ step: "userType" });
const handleSubmit = () => { next(); // ✅ Automatically navigates based on accountType };
return ( <div> <h1>Get Started</h1> <input type="email" placeholder="Email address" value={context.email || ""} // 💡 TypeScript knows this is a string onChange={(e) => setContext({ email: e.target.value })} /> <select value={context.accountType || ""} // 💡 TypeScript knows this is a string onChange={(e) => setContext({ accountType: e.target.value })} > <option value="">Choose account type</option> <option value="personal">Personal</option> <option value="business">Business</option> </select> <button onClick={handleSubmit} disabled={!context.email || !context.accountType} > Continue </button> </div> );}import { onboardingFlow } from "./flow";
function BusinessStep() { const { context, setContext, next, back } = onboardingFlow.useFlowState({ step: "business" });
return ( <div> <h1>Business Details</h1> <p>Welcome {context.email}!</p> <input placeholder="Company name" value={context.company || ""} // 💡 TypeScript knows this is a string onChange={(e) => setContext({ company: e.target.value })} /> <button onClick={back}>Back</button> <button onClick={next}>Continue</button> </div> );}import { onboardingFlow } from "./flow";
function CompleteStep() { const { context } = onboardingFlow.useFlowState({ step: "complete" });
return ( <div> <h1>All Set, {context.name}!</h1> <p>Email: {context.email}</p> {context.userType === "business" && ( <p>Company: {context.business?.companyName}</p> )} </div> );}import { Flow, createLocalStorageStore, createPersister } from "@useflow/react";import { onboardingFlow } from "./flow";
function App() { return ( <Flow flow={onboardingFlow} // ✅ Add persistence in 1 line! persister={createPersister({ store: createLocalStorageStore() })} > {({ renderStep }) => renderStep({ // 💡 TypeScript enforces all steps must be provided - can't miss any! welcome: <WelcomeStep />, userType: <UserTypeStep />, business: <BusinessStep />, personal: <PersonalStep />, complete: <CompleteStep /> })} </Flow> );}That’s it! Users can now close their browser and return exactly where they left off. No manual state management, no confusing navigation logic scattered across components.
Remote Configuration
Modify flows without redeploying.
A/B Testing
Test flow variants. See which path converts best.
Drop-in Analytics
Track completion rates and drop-offs with a few lines of code.