Getting Started
react-sequent is a lightweight React utility for building UX flows — multi-step forms, onboarding wizards, modal flows, or any UI that moves through discrete states.
The core idea: the current step decides what comes next, rather than a top-level config knowing all states in advance.
Installation
npm install react-sequent
Prerequisites: React ^16.14.0 || ^17 || ^18 || ^19
Why react-sequent?
Most flow and wizard libraries require you to define the entire state graph up front in a centralized config — every state, every transition, every branch. This works for long-lived machines that exist independently of your UI, but it adds friction for flows that live inside a component: a modal, a checkout section, an onboarding sequence.
react-sequent takes the opposite approach. The step decides what comes next. There is no separate transition map to keep in sync, no machine definition above your components. If a step needs to branch, it writes a normal if statement. If you remove a step, you delete a component — nothing else breaks.
There's a subtler difference too: flow state lives independently of the component that starts it. The flow isn't tethered to the caller's lifecycle; it runs inside the outlet's provider boundary. This means a single outlet can host multiple unrelated flows without remounting chrome, losing context, or wrapping each flow in its own closure. The library manages lifecycle state internally — consumers never manage it at all.
Quick Start
A minimal two-step flow in under 40 lines:
import {
useSequentFlow,
useSequentStep,
} from "react-sequent";
// Step 1 — decides what to render and where to go next
function Step1() {
const { advance } = useSequentStep();
return (
<div>
<h3>Step 1 — Welcome</h3>
<p>This is the first step.</p>
<button onClick={() => advance(() => Step2)}>Next</button>
</div>
);
}
// Step 2 — can go back or finish the flow
function Step2() {
const { retreat, resolve } = useSequentStep();
return (
<div>
<h3>Step 2 — Confirm</h3>
<button onClick={() => retreat()}>Back</button>
<button onClick={() => resolve("completed!")}>Finish</button>
</div>
);
}
// Host — owns the outlet and starts the flow
function App() {
const { init, status, result, SequentOutlet } = useSequentFlow();
const start = () => {
init(() => Step1);
};
React.useEffect(() => {
if (status === "idle" && result?.status === "resolved") {
console.log("Flow resolved:", result.value);
}
}, [status, result]);
return (
<div>
<SequentOutlet />
<button onClick={start}>Start Flow</button>
</div>
);
}
What's happening
useSequentFlowgives youinitand a boundSequentOutlet.<SequentOutlet>is where the flow renders. It's idle untilinit()activates it.useSequentStepis called inside step components. It returnsadvance,retreat,resolve, andabort.- When
resolve("completed!")is called,useSequentFlow().resultbecomes{ status: "resolved", value: "completed!" }.
Key Concepts
Steps own transitions
There's no centralized state map. Each step calls advance(() => NextStep) to move forward. Branching is a plain if statement:
function PaymentStep() {
const { advance, resolve } = useSequentStep();
const onSubmit = (method: string) => {
if (method === "credit") {
advance(() => CreditCardForm);
} else {
advance(() => BankTransferForm);
}
};
return <PaymentMethodPicker onSelect={onSubmit} />;
}
Async steps just work
Pass a dynamic import instead of a component — or return an element after async work. Suspense handles both:
const { advance } = useSequentStep();
// Lazy-loaded step — the outlet shows the fallback while loading
advance(() => import("./HeavyStep"));
// Async decision + props hydration — fetch first, then render the next step
advance(async () => {
const user = await fetchUser();
return <ProfileStep user={user} />;
});
Flow context
Carry shared data across steps without prop-drilling:
// Start with initial context
init(() => Step1, { name: "Alice", plan: "pro" });
// In any step — read and patch context
function Step1() {
const { advance, context } = useSequentStep();
return (
<div>
<p>Hello, {context.name}!</p>
<button onClick={() => advance(() => Step2, { step1Complete: true })}>
Next
</button>
</div>
);
}
Animated transitions
Step swaps are immediate by default. Add a transition render prop to <SequentOutlet /> to animate between steps — the outlet mounts the outgoing and incoming step together and hands you previousStep, nextStep, phase, onExited, and transitionKey. Wire them to CSS, framer-motion, or GSAP.
See Core Concepts — Animated Transitions for the phase lifecycle and a full example, or grab ready-made crossfade() and slide() factories from Transitions.
Next Steps
- useSequentFlow — flow initialization and the bound outlet
- useSequentStep — transitions, context, and flow completion
- useSequentContext — flow-level context for chrome and idle children