Skip to content

Repository files navigation

useFlow

npm versionnpm downloadslicense

Type-safe, declarative multi-step flows for React

useFlow is a lightweight library for building multi-step flows like onboarding, checkout, surveys, and wizards with full TypeScript support.

⚠️ Beta Release: useFlow is currently in beta and the API may change before v1.0.0. Breaking changes will increment the minor version. We recommend using the ~ prefix in your package.json to allow patch updates while avoiding breaking changes:

{
"dependencies": {
"@useflow/react": "~0.x.0"
}
}

Why useFlow?

Start simple, scale up. Begin with a linear flow and add conditional navigation, persistence, or callbacks as needed without refactoring your architecture.

Built for multi-step UI flows. Unlike general-purpose state machines (XState), useFlow is designed specifically for sequential user flows with simple, intuitive APIs for common patterns like navigation, history, and persistence.

Type-safe by default. Full TypeScript inference - step names autocomplete, invalid navigation throws compile-time errors, and context is fully typed throughout your flow.

Complements form libraries. useFlow works great with TanStack Form, React Hook Form, or any form library - useFlow handles the multi-step navigation and state while your form library handles validation. Perfect for multi-page forms, checkouts, onboarding, and surveys.

Features

  • 🎯 Declarative flow definitions - Define your flow in one place. Navigation logic lives in your flow config, not scattered across components.
  • 🔒 Type-safe - Full TypeScript support: Autocomplete for step names, compile-time navigation errors.
  • 🔄 Flexible navigation - Linear flows and conditional navigation.
  • 💾 Built-in persistence - Save and restore flow progress automatically.
  • 🔧 Framework-agnostic core - Built on a pure TypeScript core.
  • Lightweight - Zero dependencies.

Installation

npm install @useflow/react

Quick Example

Build a complete onboarding flow with conditional navigation:

1. Define your flow with navigation logic

import{defineFlow}from"@useflow/react";typeOnboardingContext={email?: string;accountType?: "business"|"personal";company?: string;};constonboardingFlow=defineFlow({id: "onboarding",start: "welcome",steps: {welcome: {next: "userType"},userType: {next: ["business","personal"]},// 💡 Define all possible next stepsbusiness: {next: "complete"},personal: {next: "complete"},complete: {}}// 💡 Set the context type}).with<OnboardingContext>((steps)=>({resolvers: {// 💡 Type-safe: can only return steps in next arrayuserType: (ctx)=>ctx.accountType==="business" ? steps.business// ✅ Valid
: steps.personal// ✅ Valid// steps.complete would be a TypeScript error ❌}}));

2. Create your step components

import{onboardingFlow}from"./flow";functionUserTypeStep(){const{ context, setContext, next }=onboardingFlow.useFlowState({step: "userType"});consthandleSubmit=()=>{next();// ✅ Automatically navigates based on accountType};return(<div><h1>Get Started</h1><inputtype="email"placeholder="Email address"value={context.email||""}// 💡 TypeScript knows this is a stringonChange={(e)=>setContext({email: e.target.value})}/><selectvalue={context.accountType||""}// 💡 TypeScript knows this is a stringonChange={(e)=>setContext({accountType: e.target.value})}><optionvalue="">Choose account type</option><optionvalue="personal">Personal</option><optionvalue="business">Business</option></select><buttononClick={handleSubmit}disabled={!context.email||!context.accountType}>
Continue
</button></div>);}
import{onboardingFlow}from"./flow";functionBusinessStep(){const{ context, setContext, next, back }=onboardingFlow.useFlowState({step: "business"});return(<div><h1>Business Details</h1><p>Welcome {context.email}!</p><inputplaceholder="Company name"value={context.company||""}// 💡 TypeScript knows this is a stringonChange={(e)=>setContext({company: e.target.value})}/><buttononClick={()=>back()}>Back</button><buttononClick={()=>next()}>Continue</button></div>);}
import{onboardingFlow}from"./flow";functionCompleteStep(){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>);}

3. Map your steps to components & add persistence

import{Flow,createLocalStorageStore,createPersister}from"@useflow/react";import{onboardingFlow}from"./flow";functionApp(){return(<Flowflow={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.

To start a brand-new flow at a specific step, pass initialStepId. This is type-safe for local flow definitions, and restored persisted state takes precedence when available:

<Flowflow={onboardingFlow}initialStepId="userType">{({ renderStep })=>renderStep({welcome: <WelcomeStep/>,userType: <UserTypeStep/>,business: <BusinessStep/>,personal: <PersonalStep/>,complete: <CompleteStep/>})}</Flow>

Documentation

License

MIT License. See LICENSE for details

Contributing

Contributions welcome! Please open an issue or PR.

About

Type-safe, declarative multi-step flows for React (and more)

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages