React Forum
Blog & Guide Responses
Feb 11, 2023 · 04:00 PM

[Guide] Build a Multi-step Checkout (Route State Stepper) - implementation notes

Started by Camille Duarte
guidecheckoutstepperroute stateblur submissionderived totalsredirect flowsevidencelanes

The Multi-step Checkout guide pushes a clean idea: step transitions are driven by route state, not by rewriting URLs. Blur submission keeps the flow moving with frequent feedback, derived totals are stored to stabilize UI across steps, and redirect flows handle the inevitable payment/confirmation round trips. I'm curious what this feels like under real user behavior (back/forward, refresh, double submit).

Read the guide

How did you model the route-state stepper so back/forward replays correctly without leaking hidden UI state? Did you treat blur submission as the default boundary, and how did you prevent it from feeling "phantom"? How did you store derived totals so they stay consistent across steps and during partial failures? What evidence/log lines were most valuable when debugging "why did checkout move to the next step"?

Comments (20)

Back to latest
Marisol Vega
Feb 14, 2023 · 02:14 PM

Route-state stepper was the biggest win for me because it makes the flow replayable.

If step is local component state, back/forward turns into a randomizer.

Dmitri Kline
Feb 18, 2023 · 06:06 AM

We logged step transitions as contract lines so movement wasn't mysterious:

txt
[tips] step=shipping -> step=billing reason=user:next
[tips] step=billing -> step=review reason=blur:submit valid=true
[tips] redirect from=/checkout to=/checkout/confirm reason=payment:handoff

And we rendered evidence: data-step, data-submit-lane, data-last-step-reason.

Keira Santos
Mar 01, 2023 · 03:03 AM

Counterpoint: blur submission can be hostile, especially on mobile where focus changes unexpectedly.

We used blur as the default but introduced a posture switch (submitPosture=blur|confirm) and logged it so it's explainable.

Camille Duarte
Mar 05, 2023 · 05:05 AM

Yes. Blur isn't sacred; boundaries are.

If the posture is explicit and visible, you can choose the boundary that matches the device and the user.

Grace McCoy
Mar 18, 2023 · 06:18 PM

Stored derived totals were necessary because shipping, tax, discounts, and fees touch multiple steps.

If each step computes totals, you get drift and users think the app is lying.

Jasper Nguyen
Apr 06, 2023 · 06:06 AM

Long-form: checkouts are anxiety machines. Anything that looks inconsistent destroys trust.

Derived totals as a contract reduce anxiety because the number is stable across steps, and logs make it explainable when it changes.

The guide's approach is basically: stabilize outputs first, then worry about UX polish.

Nina Kapoor
Apr 18, 2023 · 06:18 PM

Redirect flows were easier once we treated them as part of the contract and logged them. If the app jumps to confirmation without a log line, users think it glitched:

txt
[tips] redirectChain=/checkout -> /checkout/pay -> /checkout/confirm reason=payment:success
Owen Price
May 10, 2023 · 10:10 AM

Counterpoint: route state stepper can make sharing links difficult if the URL doesn't encode enough context.

We put only the step in route state (replayable), and we keep share-worthy context (cart id) in URL or stable identity.

Benji Rios
Jun 01, 2023 · 06:06 AM

We treated submit as an idempotent intent too. If blur triggers twice, it's safe. And we log dedup explicitly:

txt
[tips] intent=submit:shipping key=sub_19 lane=ok reason=dedup
Lina Ortega
Aug 19, 2023 · 07:19 PM

Docs tie-in: this is routing + forms posture merged: explicit boundaries, explicit state, visible evidence.

Related: Routing and Navigation and Forms and Validation (Best Practices).

Theo Bennett
Sep 18, 2023 · 12:12 PM

Long-form counterpoint: steppers can become a way to hide complexity rather than manage it.

If your steps have hidden prerequisites, you'll end up with brittle transitions. We made prerequisites explicit and rendered them as evidence (blocked reasons).

Salma Qureshi
Oct 15, 2023 · 03:15 PM

We rendered data-step-blocked-reason and it saved us.

Most "checkout stuck" bugs were actually prerequisites not met (address missing, payment method invalid). Evidence made that obvious.

Amina Farouk
Dec 20, 2023 · 06:18 PM

The stepper contract made tests nicer too: tests can assert step transitions without guessing timing.

If step is a visible contract, E2E becomes deterministic.

Evan Brooks
Dec 23, 2023 · 11:23 PM

Counterpoint: derived totals can make teams complacent about pricing correctness.

We still validate totals server-side and render a lane for verification. Otherwise totals become a UI illusion.

Rowan Pierce
Dec 26, 2023 · 10:22 PM

We logged total changes with reasons so users and support could understand changes:

txt
[tips] derive=totals reason=shipping:methodChange total=42.19
Jules Kwon
Dec 29, 2023 · 09:09 AM

Long-form: the guide is basically an argument that checkout should be a state machine you can narrate.

If you can't narrate it with logs and evidence, users won't trust it and support won't be able to help.

Tomas Rivera
Dec 31, 2023 · 09:09 AM

If you implement this, start by defining your step vocabulary and the evidence keys per step.

Once those exist, the rest is just wiring.

Harper Iqbal
Jul 14, 2023 · 02:14 PM

We also treated back/forward as first-class: back is a step transition, not a special case.

Once you log back transitions, you stop blaming browsers for your app's hidden state.

Priya Liao
Nov 12, 2023 · 04:04 AM

We made the stepper state typed and derived a compact evidence string from it.

Typed stepper prevented "unknown step" bugs and made logs consistent.

Talia Moss
Dec 12, 2023 · 12:12 PM

Counterpoint: stepper transitions can feel too rigid if users want to jump around.

We allowed jumping but logged jump reasons and validated prerequisites explicitly, so it still felt coherent.

Mina Calder
Dec 18, 2023 · 06:18 PM

The biggest win was reducing ad-hoc useEffect glue for checkout flows.

Once step transitions are explicit, a lot of effect glue disappears because the contract tells you what should happen.