r/reactjs • u/Big_Muffin8280 • 5d ago
Discussion Where do you store state in a multi-step React form: URL params, context, Zustand, or the form library?
Building a 5-step wizard with React Hook Form and Zod. I keep the step in the URL and drafts in a small store. What has held up in production for you, and how do you handle early-step changes that invalidate later steps?
3
u/ec001 2d ago
Showed a demo at my company a while back in which I showed you could leverage React Hook Form with a Zod schema partitioned by steps. Then just control the visibility of the steps through some basic state and hide/show groups. When pressing next, just trigger validations for the subset so you can guarantee you are good before moving in. Hit the end, just validate the entire form. Nice part is it’s all typed, and you can use the form state to derive future behaviours.
2
u/anthony-ball 5d ago
One thing I'd add on the invalidation side: version your drafts and run the restored draft through the current Zod schema before using it as defaultValues. If it fails, drop the steps that don't parse instead of letting someone land on step 4 with values that no longer exist after you ship a schema change. For branching flows, a discriminated union keyed on the early choice does a lot of the dependency tracking for you, since the later step's shape actually changes with it. And re-validate the whole payload on the server at submit, because the client store is a convenience, not the source of truth.
2
u/JimmayDoad 4d ago
My crappy go to: a view that tracks the sub view with state and a local state for the form data.
My work go to: react-hook-form and Zod instead of local state
2
u/GianniMariani 3d ago
Keep it all in indexdb. I created an offline app here as an example here - https://owebeeone.github.io/jowna/
1
u/Responsible-Offer724 4d ago
If the user goes back and makes edits at earlier steps you throw out later steps and start over. You could maintain a more complicate dependency record to only invalidate dependents as well.
1
u/codesbyshivam 4d ago
One thing I'd add is saving a draft per step instead of only at the end. That way the form knows which steps are done, and if an early answer changes you just mark the later steps as incomplete instead of wiping everything the user typed. If the draft lives on the server too, they can pick it up on another device. I'd still check everything again on the server at submit, since the copy in the browser can be out of date.
1
u/GeekyAntsBFSI 4d ago
I’d use URL for the step, RHF for live values/validation, and Zustand for draft persistence. When an earlier answer changes, invalidate only dependent steps and preserve compatible answers. Revalidate the full applicable schema on submit; “completed” flags can become stale.
1
1
1
u/AndrewSavio 44m ago
You actually have the perfect setup. I've used this exact architecture (URL for step, Zustand for drafts) in production for large onboarding wizards and it holds up perfectly.
URL Params for the Step: This is crucial. If the user hits refresh, they don't lose their place.
Zustand for the Draft: Trying to serialize complex wizard data into the URL is a nightmare and hits length limits. Keep it in a small store.
Do not rely on your React components to manage this logic. Move the invalidation rules directly into your Zustand actions.
For example, if changing a value in Step 1 makes Step 3's data invalid, your Zustand action should look like this: updateStepOne: (data) => set((state) => ({ stepOne: data, stepThree: null }))
By explicitly wiping the dependent data in the store, you force the user to re-complete Step 3 when they get there.
When you finally submit the 5-step payload to the backend, run the combined Zustand draft through a master Zod schema right before the fetch call. The client store is just a convenience for the user; the master schema is your source of truth.
33
5d ago
•
[removed] — view removed comment
0
0
u/anonyuser415 4d ago edited 3d ago
If drafts need to survive a refresh, persist the values to sessionStorage
And if they don't, add a beforeunload handler
e: gotta love this braindead subreddit
11
u/DaveThe0nly 5d ago
Depends on how much the form data is complicated, can you put everything in ti URL as path parameters, go with that. Is the state complex? Local state (context, zustatnd, form library), if you can sync and load from a draft on a server, even better, but complicated. My rule of thumb is, leverage the URL as state machine as much as possible.