Skip to main content

Base Component

Creating the main form component with step navigation.

Overview

The base component is the entry point for our multi-step form. It:

  • Creates and manages the form instance
  • Handles step navigation
  • Renders the current step
  • Manages form submission

Creating the Form Instance

First, create the form instance using useMemo to prevent recreation on each render:

src/components/CreditApplicationForm.tsx
import { useMemo } from 'react';
import { createCreditApplicationForm } from './schemas/create-credit-application-form';

function CreditApplicationForm() {
// Create form instance once
const form = useMemo(() => createCreditApplicationForm(), []);

// ...
}
Important

Always wrap form creation in useMemo. Without it, the form would be recreated on every render, losing all user input.

Step Navigation

For multi-step forms, you need to track the current step and provide navigation:

import { useState } from 'react';

function CreditApplicationForm() {
const form = useMemo(() => createCreditApplicationForm(), []);

const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 6;

const goToNextStep = () => {
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};

const goToPreviousStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};

// ...
}

Rendering Steps

Render the appropriate step component based on currentStep:

import { BasicInfoForm } from './steps/BasicInfoForm';
import { PersonalInfoForm } from './steps/PersonalInfoForm';
import { ContactInfoForm } from './steps/ContactInfoForm';
import { EmploymentForm } from './steps/EmploymentForm';
import { AdditionalInfoForm } from './steps/AdditionalInfoForm';
import { ConfirmationForm } from './steps/ConfirmationForm';

function CreditApplicationForm() {
// ... form and state setup

return (
<div>
{/* Step indicator */}
<div className="mb-6">
Step {currentStep} of {totalSteps}
</div>

{/* Current step content */}
<div className="bg-white p-6 rounded-lg shadow">
{currentStep === 1 && <BasicInfoForm control={form} />}
{currentStep === 2 && <PersonalInfoForm control={form} />}
{currentStep === 3 && <ContactInfoForm control={form} />}
{currentStep === 4 && <EmploymentForm control={form} />}
{currentStep === 5 && <AdditionalInfoForm control={form} />}
{currentStep === 6 && <ConfirmationForm control={form} />}
</div>

{/* Navigation buttons */}
<div className="flex justify-between mt-6">
<button onClick={goToPreviousStep} disabled={currentStep === 1}>
Previous
</button>

{currentStep < totalSteps ? (
<button onClick={goToNextStep}>Next</button>
) : (
<button onClick={handleSubmit}>Submit</button>
)}
</div>
</div>
);
}

Passing Form to Steps

Each step component receives the entire form as a control prop:

interface StepProps {
control: GroupNodeWithControls<CreditApplicationForm>;
}

function BasicInfoForm({ control }: StepProps) {
return (
<div>
<FormField control={control.loanType} />
<FormField control={control.loanAmount} />
{/* ... */}
</div>
);
}

This approach:

  • Gives each step access to all form fields
  • Enables cross-step dependencies (e.g., showing fields based on values from other steps)
  • Keeps a single source of truth for form state

Form Submission

Handle form submission when the user completes all steps:

function CreditApplicationForm() {
// ... form and state setup

const handleSubmit = async () => {
// Get all form values
const values = form.getValue();

try {
// Send to server
const response = await fetch('/api/applications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});

if (response.ok) {
alert('Application submitted successfully!');
}
} catch (error) {
console.error('Submission failed:', error);
}
};

// ...
}

Complete Example

Here's the complete base component:

src/components/CreditApplicationForm.tsx
import { useMemo, useState } from 'react';
import type { GroupNodeWithControls } from '@reformer/core';
import { createCreditApplicationForm } from './schemas/create-credit-application-form';
import type { CreditApplicationForm } from './types';

// Step components
import { BasicInfoForm } from './steps/BasicInfoForm';
import { PersonalInfoForm } from './steps/PersonalInfoForm';
import { ContactInfoForm } from './steps/ContactInfoForm';
import { EmploymentForm } from './steps/EmploymentForm';
import { AdditionalInfoForm } from './steps/AdditionalInfoForm';
import { ConfirmationForm } from './steps/ConfirmationForm';

const STEPS = [
{ id: 1, title: 'Loan Info' },
{ id: 2, title: 'Personal' },
{ id: 3, title: 'Contact' },
{ id: 4, title: 'Employment' },
{ id: 5, title: 'Additional' },
{ id: 6, title: 'Confirmation' },
];

function CreditApplicationForm() {
// Create form instance
const form = useMemo(() => createCreditApplicationForm(), []);

// Step navigation state
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = STEPS.length;

// Navigation handlers
const goToNextStep = () => {
if (currentStep < totalSteps) {
setCurrentStep(currentStep + 1);
}
};

const goToPreviousStep = () => {
if (currentStep > 1) {
setCurrentStep(currentStep - 1);
}
};

const goToStep = (step: number) => {
if (step >= 1 && step <= totalSteps) {
setCurrentStep(step);
}
};

// Form submission
const handleSubmit = async () => {
const values = form.getValue();

try {
const response = await fetch('/api/applications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
});

if (response.ok) {
const data = await response.json();
alert(`Application submitted! ID: ${data.id}`);
}
} catch (error) {
console.error('Submission failed:', error);
}
};

return (
<div className="max-w-4xl mx-auto p-6">
{/* Step indicator */}
<div>
{STEPS.map((step) => (
<button
key={step.id}
onClick={() => goToStep(step.id)}
className={`px-4 py-2 rounded ${
currentStep === step.id ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700'
}`}
>
{step.id}. {step.title}
</button>
))}
</div>

{/* Current step content */}
<div className="bg-white p-8 rounded-lg shadow-md">
{currentStep === 1 && <BasicInfoForm control={form} />}
{currentStep === 2 && <PersonalInfoForm control={form} />}
{currentStep === 3 && <ContactInfoForm control={form} />}
{currentStep === 4 && <EmploymentForm control={form} />}
{currentStep === 5 && <AdditionalInfoForm control={form} />}
{currentStep === 6 && <ConfirmationForm control={form} />}
</div>

{/* Navigation buttons */}
<div className="flex justify-between mt-6">
<button
onClick={goToPreviousStep}
disabled={currentStep === 1}
className="px-6 py-2 bg-gray-300 rounded disabled:opacity-50"
>
Previous
</button>

{currentStep < totalSteps ? (
<button onClick={goToNextStep} className="px-6 py-2 bg-blue-600 text-white rounded">
Next
</button>
) : (
<button onClick={handleSubmit} className="px-6 py-2 bg-green-600 text-white rounded">
Submit Application
</button>
)}
</div>

{/* Progress info */}
<div className="mt-4 text-center text-sm text-gray-600">
Step {currentStep} of {totalSteps}{Math.round((currentStep / totalSteps) * 100)}%
complete
</div>
</div>
);
}

export default CreditApplicationForm;

Next Steps

Now that we have the base component, let's create the individual step components that render the form fields.