Skip to content

Abstract onboarding wizard into reusable FormWizard - #663

Merged
Isoscelestial merged 32 commits into
developfrom
feature/588-abstract-formwizard
Aug 7, 2026
Merged

Abstract onboarding wizard into reusable FormWizard#663
Isoscelestial merged 32 commits into
developfrom
feature/588-abstract-formwizard

Conversation

@SinhSinhAn

@SinhSinhAn SinhSinhAn commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

Closes #588

Summary

Mostly internal changes that will make things a lot easier to develop in the future.

  • Extracted the onboarding multi-step wizard into reusable and customizable form.Wizard and form.WizardStep components that any form can use
  • Used new wizard components on create club page (/directory/create) as an example
  • Added fake "Create Account" step to onboarding as a UX trick to encourage users to actually complete onboarding
  • Improve form wizard validation handling
    • First name, major, classification, graduation date, and UTD email are all consistently required across onboarding and account settings
    • Fixed a bug where validation blocked backward navigation (now only blocks forward)
    • Graduation date now isn't required if classification is faculty or staff

How it works

The wizard registers as a TanStack Form formComponent, so consumers use it as form.Wizard / form.WizardStep:

The wizard renders a consistent UI that consists of: a MUI Stepper, steps with slide transitions, dynamic height, and navigation buttons.

The FormWizardStep component is NOT rendered normally, as FormWizard uses its children prop to construct the array of steps for handling. Each step also corresponds to Tanstack Form v1.33's new FormGroup for validation; entering an object key that matches your form's schema into the step's name prop ensures that step's fields are validated correctly, allowing the wizard to handle progression through the stepper and navigation buttons correctly.

Example

const schema = z.object({
  step1: z.object({
    name: z.string(),
  }),
  step2: z.object({
    email: z.email(),
  }),
});
type Schema = z.infer<typeof schema>;
const form = useAppForm({
  validators: { onSubmit: schema },
});
return (
  <form.AppForm>
    <form.Wizard onComplete={() => router.push('/')}>
      <form.WizardStep name="welcome" hidden>
        <h1>Welcome!</h1>
      </form.WizardStep>
      <form.WizardStep<Schema> name="step1" label="Step 1"
        {/* ...Step 1 form fields... */}
      </form.WizardStep>
      <form.WizardStep<Schema> name="step2" label="Step 2"
        {/* ...Step 2 form fields... */}
      </form.WizardStep>
    </form.Wizard>
  </form.AppForm>
);

Files changed

File What
src/components/form/FormWizard/ New directory with types, context, Wizard, and WizardStep
src/components/form/FormWizard.tsx Deleted (replaced by directory)
src/utils/form.ts Registered Wizard + WizardStep in formComponents
src/components/getting-started/OnboardingForm.tsx Rewritten to use the new wizard
src/components/getting-started/OnboardingFormStep.tsx Deleted (content moved inline)
src/app/directory/create/CreateClubForm.tsx Rewritten to use the new wizard
src/components/form/*.tsx Misc. style fixes
src/components/settings/forms/UserInfo.tsx Fields made consistent with onboarding
src/utils/formSchemas.ts Restructured onboarding and create club schemas for how Tanstack Form's FormGroups expect it to be structured
src/utils/Subscribe.tsx New component so its children can subscribe and render if a store changes
src/utils/useIsMounted.ts Performant hook to return a flag on whether React has finished loading client-side

Extract the ~400-line OnboardingForm wizard into a reusable form.Wizard
and form.WizardStep component system that integrates with TanStack Form's
formComponents registry. This enables multi-step wizards for onboarding,
club match, club creation, and event creation forms.

Key changes:
- Create FormWizard/ directory with types, context, and components
- Register Wizard and WizardStep as formComponents in form.ts
- Fix validation to only block forward navigation (not backward)
- Auto-advance to finish step after successful form submission
- Rewrite OnboardingForm from ~400 lines to ~260 lines
- Delete OnboardingFormStep (content moved inline to WizardStep children)

Closes #588
@vercel

vercel Bot commented Mar 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clubs Ready Ready Preview, Comment Jul 26, 2026 5:01am

Request Review

@SinhSinhAn

Copy link
Copy Markdown
Contributor Author

@Isoscelestial, Hey Isaac wanted to follow up on this, can you check it when you get the chance?

@Isoscelestial Isoscelestial left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick look through, I like how it's looking so far! After you fix these things, I'll do another more in-depth review.

Comment thread src/components/form/FormWizard/types.ts Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/getting-started/OnboardingForm.tsx Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Co-authored-by: Isoscelestial <isoscelestial@gmail.com>
Moved startStep and finishStep from Wizard props into WizardStep children using startStep and finishStep boolean props, so the API is consistent with how body steps are declared. Restored type safety on StepConfig using a discriminated union so start and finish variants cannot have fields. Fixed the Continue button on the finish screen not working, and made the wizard wait for the API call to finish before advancing to the finish step. Fixed date picker dark mode background from neutral-900 to neutral-800.

@Isoscelestial Isoscelestial left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, love to see it functional! Sorry that I'm requesting a lot of changes...

Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/getting-started/OnboardingForm.tsx
Comment thread src/components/form/FormWizard/FormWizard.tsx Outdated
Comment thread src/components/form/FormWizard/types.ts Outdated
Comment thread src/components/form/FormWizard/types.ts Outdated
Comment thread src/components/form/FormWizard/types.ts Outdated
@TyHil

TyHil commented Jun 10, 2026

Copy link
Copy Markdown
Member

Adding a note here to copy these changes over to Notebook after merge.

@SinhSinhAn

Copy link
Copy Markdown
Contributor Author

Resolved all FormWizard-related threads since we're taking a different approach: fixing the two real bugs directly in OnboardingForm.tsx instead of extracting the full abstraction.

What's fixed (on fix/683-google-event-guard):

  1. Backward navigation no longer blocked by validation. Removed the validateFields() / currentFieldsValid() gate from handleBack and removed !currentFieldsValid() from the Back button's disabled prop.
  2. ResizeObserver leak. The callback ref's return value was silently ignored by React. Now stores the observer in a useRef and disconnects properly on re-attach.

Left unresolved: The thread about changing onChange to onSubmit validator timing is a separate concern worth discussing independently.

The FormWizard abstraction can be revisited when a second wizard consumer actually exists (club creation, event creation, etc.), at which point the real API surface will be clearer.

Co-authored-by: Isoscelestial <isoscyoung@gmail.com>
@Isoscelestial

Isoscelestial commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Resolved all FormWizard-related threads since we're taking a different approach: fixing the two real bugs directly in OnboardingForm.tsx instead of extracting the full abstraction.

You already made the full abstraction. At this point, we are just fixing bugs and code quality issues. I am unresolving all the threads so that they can be fixed. If you'd like help with some of the threads, I'd be more than happy to tackle some of them!

What's fixed (on fix/683-google-event-guard):

Not sure where this branch name came from. If your AI tends to hallucinate, I ask that you at the very least double check your code and comments before sending them to be reviewed.

The thread about changing onChange to onSubmit validator timing is a separate concern worth discussing independently.

I'd like these changes to be made now, as we've figured out a solution and are just waiting for the code to be written.

Related PR: UTDNebula/utd-notebook#183

The FormWizard abstraction can be revisited when a second wizard consumer actually exists (club creation, event creation, etc.), at which point the real API surface will be clearer.

As we discussed earlier, we are planning to have a "second wizard consumer" by using this abstracted system with the club matching form, the club creation form, and event creation form.

@Isoscelestial

Isoscelestial commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

So I may have gone overboard with my additions 💀 I edited this PR's initial comment to document my changes. But yeah @SinhSinhAn your abstraction absolutely works, I just wanted to make the actual wizard more robust and customizable.

For other people reviewing this PR, you can just test the /get-started and /directory/create pages. We'll test the wizard customization features when we move this stuff over to the Nebula Library in another PR (per UTDNebula/nebula-library#9).

@Isoscelestial
Isoscelestial self-requested a review July 23, 2026 04:11
@SinhSinhAn

Copy link
Copy Markdown
Contributor Author

Bro this is your PR now 💀💀

Congrats and great job

…epIndex, fix dispatchWizardAction by making it async, fix inaccurate variable names
@AbhiramTadepalli

Copy link
Copy Markdown
Contributor

Been doing some testing:
/get-started

  1. I can click the clubs logo to go home and not be redirected to /get-started, idk if changing that was in scope for this PR
  2. When I go to the home screen / and then use browser nav (in firefox) to go back, the "Get Started" text doesn't show, and clicking the Start button does nothing only if I have not properly filled in my name (navigate to /get-started -> go to Name card -> clear our your name -> go home -> browser nav back to /get-started). Navigating directly to the url does not have this error.
  3. College Info and Contact Email fields do not persist after refresh while name does, is that because Name is pre-filled from the account details (google)?
  4. Why is the placeholder text for Graduation date "MMMM
  5. On creation of account, if I instead navigate to settings and fill out my info there, and then come back to the /get-started page via browser navigation, it will not pre-fill my settings info.
  6. Could make this one card's text have less start padding:
image

/directory/create seems great!

@Isoscelestial

Copy link
Copy Markdown
Contributor
1. I can click the clubs logo to go home and not be redirected to `/get-started`, idk if changing that was in scope for this PR

Yeah I figure if people truly are desperate to skip onboarding, may as well just let them lol. Do we want to strictly require onboarding?

2. When I go to the home screen `/` and then use browser nav (in firefox) to go back, the "Get Started" text doesn't show, and clicking the Start button does nothing **only if** I have not properly filled in my name (navigate to /get-started -> go to Name card -> clear our your name -> go home -> browser nav back to /get-started). Navigating directly to the url does not have this error.

Seems to be an issue on all pages (for instance, using browser nav to go to the home page makes the cards not fill. It appears that even the page.tsx file (at least for /get-started) doesn't even re-render, which is really odd. I believe something changed in a recent Next.js update.

20 minutes later...

Fixed by updating to Next.js to 16.2.7! We were on 16.2.6, so I went ahead and updated package.json. Looks like it only happened in dev mode (production is fine). See vercel/next.js#93486, which was merged in vercel/next.js#93492.

3. College Info and Contact Email fields do not persist after refresh while name does, is that because Name is pre-filled from the account details (google)?

Yeah. There's nothing to make values persist after refresh

4. Why is the placeholder text for Graduation date "MMMM YYYY"

MUI's default placeholder text. I don't think it's that jarring, what do you think it should be instead?

5. On creation of account, if I instead navigate to settings and fill out my info there, and then come back to the `/get-started` page **_via browser navigation,_** it will not pre-fill my settings info.

What an edge case. Well, that'd make sense since it queries that info during SSR and that's cached. Looks like a global issue too, happens on other pages (try navigating back forward to settings, it doesn't show the updated value until you refresh).

Could be resolved by adding a Tanstack Query QueryClient that invalidates all past queries whenever pathname changes? Unless Tanstack Query has another solution (which I couldn't find). This would be something for another PR. Although I'd argue this is an edge-case easily resolved by just refreshing; heck, I think I've even seen github.com have this issue.

6. Could make this one card's text have less start padding:

For some reason it has ml-3.5, which is such an oddly specific number 😭 I wonder what I was thinking when I put that. Anyway I reduced it to ml-2

@Isoscelestial Isoscelestial left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works by my testing, thanks @SinhSinhAn!

@Isoscelestial
Isoscelestial merged commit fdca33d into develop Aug 7, 2026
3 checks passed
@Isoscelestial
Isoscelestial deleted the feature/588-abstract-formwizard branch August 7, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Abstract onboarding experience to FormWizard

4 participants