Quick Start
useFlow is a declarative, type-safe library for building multi-step flows in React with built-in persistence. Whether you’re building onboarding, checkout, surveys, or wizards, useFlow eliminates the complexity while providing exceptional developer experience.
This guide will walk you through building a complete onboarding flow from scratch. You’ll learn the fundamentals of useFlow by creating a working application.
What we’re building
Section titled “What we’re building”A simple 3-step onboarding flow:
- Welcome - Greet the user
- Profile - Collect name and email
- Complete - Show confirmation
Prerequisites
Section titled “Prerequisites”- Node.js 18+ or Bun
- React 18+
- Basic knowledge of React hooks
Build your first flow
Section titled “Build your first flow”-
Install useFlow
Section titled “Install useFlow”Terminal window npm install @useflow/reactTerminal window bun add @useflow/reactTerminal window yarn add @useflow/reactTerminal window pnpm add @useflow/react -
Define your flow
Section titled “Define your flow”Create a new file
flow.tsand define your flow structure:flow.ts import { defineFlow } from "@useflow/react";// Define the context type - this is the data that flows through all stepstype OnboardingContext = {name: string;email: string;};// Define your flow with steps and transitionsexport const onboardingFlow = defineFlow({id: "onboarding",start: "welcome",steps: {welcome: {next: "profile",},profile: {next: "complete",},complete: {// No next property = final step},},});Key concepts:
- id - Unique identifier for this flow (used for persistence)
- start - The first step to show
- steps - Object mapping step IDs to their configuration
- next - Which step comes after this one
-
Create step components
Section titled “Create step components”Create components for each step. Each component uses the
useFlowState()hook to access flow state and navigation.Welcome Step
Section titled “Welcome Step”WelcomeStep.tsx import { useFlowState } from "@useflow/react";export function WelcomeStep() {const { next } = useFlowState();return (<div><h1>Welcome to Our App!</h1><p>Let's get you set up in just a few steps.</p><button onClick={() => next()}>Get Started</button></div>);}Profile Step
Section titled “Profile Step”ProfileStep.tsx import { useFlowState } from "@useflow/react";import { useState } from "react";type OnboardingContext = {name: string;email: string;};export function ProfileStep() {const { context, next, back, setContext } = useFlowState<OnboardingContext>();// Local state for form validationconst [errors, setErrors] = useState<Record<string, string>>({});const handleSubmit = () => {const newErrors: Record<string, string> = {};if (!context.name) newErrors.name = "Name is required";if (!context.email) newErrors.email = "Email is required";if (context.email && !context.email.includes("@")) {newErrors.email = "Invalid email";}if (Object.keys(newErrors).length > 0) {setErrors(newErrors);return;}next(); // Proceed to next step};return (<div><h1>Create Your Profile</h1><div><label>Name<inputtype="text"value={context.name}onChange={(e) => setContext({ name: e.target.value })}placeholder="Your name"/></label>{errors.name && <span style={{ color: "red" }}>{errors.name}</span>}</div><div><label>Email<inputtype="email"value={context.email}onChange={(e) => setContext({ email: e.target.value })}placeholder="your@email.com"/></label>{errors.email && <span style={{ color: "red" }}>{errors.email}</span>}</div><div><button onClick={() => back()}>Back</button><button onClick={handleSubmit}>Continue</button></div></div>);}What’s happening:
- context - Access shared flow data
- setContext() - Update context (partial updates merge automatically)
- next() - Navigate to the next step
- back() - Navigate to the previous step
Complete Step
Section titled “Complete Step”CompleteStep.tsx import { useFlowState } from "@useflow/react";type OnboardingContext = {name: string;email: string;};export function CompleteStep() {const { context } = useFlowState<OnboardingContext>();return (<div><h1>All Set, {context.name}!</h1><p>We've sent a confirmation to {context.email}</p><button onClick={() => window.location.href = "/dashboard"}>Go to Dashboard</button></div>);} -
Render your flow
Section titled “Render your flow”Now put it all together in your main App component:
App.tsx import { Flow } from "@useflow/react";import { onboardingFlow } from "./flow";import { WelcomeStep } from "./WelcomeStep";import { ProfileStep } from "./ProfileStep";import { CompleteStep } from "./CompleteStep";export function App() {return (<Flowflow={onboardingFlow}initialContext={{name: "",email: "",}}onComplete={() => {console.log("Onboarding completed!");}}>{({ renderStep }) => (<div style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}>{renderStep({welcome: <WelcomeStep />,profile: <ProfileStep />,complete: <CompleteStep />,})}</div>)}</Flow>);}Flow Component Props:
- flow - Your flow definition from defineFlow()
- initialContext - Starting values for your context
- onComplete - Callback when the flow finishes
- children - Render function receiving flow state
-
Test it out
Section titled “Test it out”Run your app and try the flow:
- Click “Get Started” on the welcome screen
- Fill in your name and email
- See the completion message
Try these interactions:
- Click “Back” to return to the welcome step
- Leave fields empty to see validation
- Enter an invalid email
Bonus: add persistence (30 seconds)
Section titled “Bonus: add persistence (30 seconds)”import { Flow, createLocalStorageStore, createPersister } from "@useflow/react";
export function App() { return ( <Flow flow={onboardingFlow} // ✅ Add persistence in 1 line! persister={createPersister({ store: createLocalStorageStore(localStorage) })} initialContext={{ name: "", email: "" }} > {/* 💡 Render your steps here */} </Flow> );}Bonus: add analytics (15 seconds)
Section titled “Bonus: add analytics (15 seconds)”<Flow flow={onboardingFlow} persister={persister} initialContext={{ name: "", email: "" }} // ✅ Track with any analytics platform onNext={({ from, to }) => analytics.track('step_completed', { from, to })} // ✅ Track completion onComplete={() => analytics.track('onboarding_completed')}> {/* ... */}</Flow>What you’ve learned
Section titled “What you’ve learned”✅ How to define a flow with defineFlow()
✅ How to use the useFlowState() hook
✅ How to navigate with next() and back()
✅ How to manage context with setContext()
✅ How to render steps with the Flow component
✅ How to add persistence in 1 line
✅ How to add analytics tracking instantly
Next steps
Section titled “Next steps”Now that you’ve built your first flow, explore more advanced features:
Complete example
Section titled “Complete example”View the complete working code
// flow.tsimport { defineFlow } from "@useflow/react";
type OnboardingContext = { name: string; email: string;};
export const onboardingFlow = defineFlow({ id: "onboarding", start: "welcome", steps: { welcome: { next: "profile" }, profile: { next: "complete" }, complete: {}, },});
// WelcomeStep.tsximport { useFlowState } from "@useflow/react";
export function WelcomeStep() { const { next } = useFlowState();
return ( <div> <h1>Welcome to Our App!</h1> <p>Let's get you set up in just a few steps.</p> <button onClick={() => next()}>Get Started</button> </div> );}
// ProfileStep.tsximport { useFlowState } from "@useflow/react";import { useState } from "react";
type OnboardingContext = { name: string; email: string;};
export function ProfileStep() { const { context, next, back, setContext } = useFlowState<OnboardingContext>();
// Local state for form validation const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = () => { const newErrors: Record<string, string> = {};
if (!context.name) newErrors.name = "Name is required"; if (!context.email) newErrors.email = "Email is required"; if (context.email && !context.email.includes("@")) { newErrors.email = "Invalid email"; }
if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; }
next(); // Proceed to next step };
return ( <div> <h1>Create Your Profile</h1>
<div> <label> Name <input type="text" value={context.name} onChange={(e) => setContext({ name: e.target.value })} placeholder="Your name" /> </label> {errors.name && <span style={{ color: "red" }}>{errors.name}</span>} </div>
<div> <label> Email <input type="email" value={context.email} onChange={(e) => setContext({ email: e.target.value })} placeholder="your@email.com" /> </label> {errors.email && <span style={{ color: "red" }}>{errors.email}</span>} </div>
<div> <button onClick={() => back()}>Back</button> <button onClick={handleSubmit}>Continue</button> </div> </div> );}
// CompleteStep.tsximport { useFlowState } from "@useflow/react";
type OnboardingContext = { name: string; email: string;};
export function CompleteStep() { const { context } = useFlowState<OnboardingContext>();
return ( <div> <h1>All Set, {context.name}!</h1> <p>We've sent a confirmation to {context.email}</p> <button onClick={() => window.location.href = "/dashboard"}> Go to Dashboard </button> </div> );}
// App.tsximport { Flow } from "@useflow/react";import { onboardingFlow } from "./flow";import { WelcomeStep } from "./WelcomeStep";import { ProfileStep } from "./ProfileStep";import { CompleteStep } from "./CompleteStep";
export function App() { return ( <Flow flow={onboardingFlow} initialContext={{ name: "", email: "", }} onComplete={() => { console.log("Onboarding completed!"); }} > {({ renderStep }) => ( <div style={{ maxWidth: "600px", margin: "0 auto", padding: "20px" }}> {renderStep({ welcome: <WelcomeStep />, profile: <ProfileStep />, complete: <CompleteStep />, })} </div> )} </Flow> );}