Skip to main content

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

  1. useSequentFlow gives you init and a bound SequentOutlet.
  2. <SequentOutlet> is where the flow renders. It's idle until init() activates it.
  3. useSequentStep is called inside step components. It returns advance, retreat, resolve, and abort.
  4. When resolve("completed!") is called, useSequentFlow().result becomes { 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