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

Overview

useFlow is built around a few core concepts that work together to create type-safe, declarative multi-step flows. Understanding these concepts will help you build robust flows efficiently.

A flow is a declarative definition of a multi-step process. It defines:

  • Steps: The individual stages in your flow
  • Transitions: How users move between steps
  • Starting point: Where the flow begins

Flows are JSON-serializable and can be:

  • Defined in your code
  • Fetched from a remote API
  • Generated dynamically based on conditions
const onboardingFlow = defineFlow({
id: 'onboarding',
start: 'welcome',
steps: {
welcome: { next: 'profile' },
profile: { next: 'preferences' },
preferences: { next: 'complete' },
complete: {} // Terminal step
}
});

Learn more about Flows →

Steps represent individual stages in your flow. Each step:

  • Has a unique identifier (the key in the steps object)
  • Defines its next destination(s)
  • Renders a React component

Steps can be:

  • Linear: Single next destination
  • Branching: Multiple possible next destinations
  • Terminal: No next destination (flow complete)
// Render current step
<Flow flow={onboardingFlow}>
{({ renderStep }) => renderStep({
welcome: <WelcomeStep />,
profile: <ProfileStep />,
preferences: <PreferencesStep />,
complete: <CompleteStep />
})}
</Flow>

Learn more about Steps →

Context is the shared state that flows through your entire multi-step process. It:

  • Stores user data collected across steps
  • Is type-safe (inferred from your flow definition)
  • Can be updated as users progress
  • Persists automatically when configured
// Define context type
type OnboardingContext = {
name: string;
email: string;
preferences: string[];
};
// Update context when navigating
const { next, context } = flow.useFlowState({ step: 'profile' });
next({
name: 'Alice',
email: 'alice@example.com'
});

Learn more about Context →

Navigation controls how users move through your flow:

  • Forward: next() and skip()
  • Backward: back()
  • Reset: reset() to start over

Navigation can be:

  • Context-driven: Automatic based on context (using resolvers)
  • Component-driven: Explicit from user actions
  • Programmatic: Triggered by your code
const { next, back, skip, canGoBack } = flow.useFlowState({ step: 'profile' });
// Move forward with data
next({ name: 'Alice' });
// Skip a step
skip();
// Go back if possible
if (canGoBack) {
back();
}

Learn more about Navigation →

Here’s how these concepts combine in a complete flow:

import { defineFlow, Flow } from '@useflow/react';
// 1. Define context type
type SurveyContext = {
answer1?: string;
answer2?: string;
};
// 2. Define the flow structure
const surveyFlow = defineFlow({
id: 'survey',
start: 'intro',
steps: {
intro: { next: 'question1' },
question1: { next: 'question2' },
question2: { next: 'results' },
results: {}
}
}).with<SurveyContext>({});
// 3. Create a step component
function Question1Step() {
const { next, context } = surveyFlow.useFlowState({ step: 'question1' });
return (
<div>
<h1>Question 1</h1>
<button onClick={() => next({ answer1: 'Yes' })}>
Yes
</button>
</div>
);
}
// 4. Render the flow
function SurveyApp() {
return (
<Flow
flow={surveyFlow}
initialContext={{ answer1: undefined, answer2: undefined }}
>
{({ renderStep }) => renderStep({
intro: <IntroStep />,
question1: <Question1Step />,
question2: <Question2Step />,
results: <ResultsStep />
})}
</Flow>
);
}

useFlow is designed with a layered architecture:

Framework-agnostic core functionality:

  • Type definitions
  • Flow reducer (state management)
  • Persistence system
  • Storage adapters

React-specific implementations:

  • defineFlow() function
  • <Flow> component
  • useFlowState() hook
  • <FlowProvider> for global config

This separation means:

  • ✅ The core can be used with any framework
  • ✅ React package adds zero overhead to core logic
  • ✅ Type safety is preserved across layers

Every aspect of useFlow is designed for type safety:

  • Step names are validated at compile time
  • Context updates are type-checked
  • Navigation targets must be valid
  • No runtime surprises

Define what your flow is, not how it works:

  • Flow structure is data, not code
  • Steps declare transitions, not logic
  • Branching is configuration, not conditions

Clear boundaries between:

  • Flow definition: Structure and transitions
  • Step components: UI and interactions
  • Context: Shared state
  • Navigation: Movement logic

Built-in support for:

  • Browser storage (localStorage, sessionStorage)
  • Custom storage backends
  • Automatic or manual saving
  • State restoration on mount

Now that you understand the core concepts, dive deeper into each one:

  • Flows - Define flow structure and transitions
  • Steps - Create and organize step components
  • Context - Manage shared state
  • Navigation - Control flow movement

Or jump to practical guides: