Skip to main content

Project Structure

Organize your forms for scalability and maintainability using colocation β€” keeping related files together.

src/
β”œβ”€β”€ components/
β”‚ └── ui/ # Reusable UI components
β”‚ β”œβ”€β”€ FormField.tsx # Field wrapper component
β”‚ β”œβ”€β”€ FormArrayManager.tsx # Dynamic array manager
β”‚ └── ... # Input, Select, Checkbox, etc.
β”‚
β”œβ”€β”€ forms/
β”‚ └── [form-name]/ # Form module
β”‚ β”œβ”€β”€ type.ts # Main form type (combines step types)
β”‚ β”œβ”€β”€ schema.ts # Main schema (combines step schemas)
β”‚ β”œβ”€β”€ validators.ts # Validators (steps + cross-step)
β”‚ β”œβ”€β”€ behaviors.ts # Behaviors (steps + cross-step)
β”‚ β”œβ”€β”€ [FormName]Form.tsx # Main form component
β”‚ β”‚
β”‚ β”œβ”€β”€ steps/ # Step modules (wizard)
β”‚ β”‚ β”œβ”€β”€ loan-info/
β”‚ β”‚ β”‚ β”œβ”€β”€ type.ts # Step-specific types
β”‚ β”‚ β”‚ β”œβ”€β”€ schema.ts # Step schema
β”‚ β”‚ β”‚ β”œβ”€β”€ validators.ts # Step validators
β”‚ β”‚ β”‚ β”œβ”€β”€ behaviors.ts # Step behaviors
β”‚ β”‚ β”‚ └── BasicInfoForm.tsx # Step component
β”‚ β”‚ β”‚
β”‚ β”‚ β”œβ”€β”€ personal-info/
β”‚ β”‚ β”‚ β”œβ”€β”€ type.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ schema.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ validators.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ behaviors.ts
β”‚ β”‚ β”‚ └── PersonalInfoForm.tsx
β”‚ β”‚ β”‚
β”‚ β”‚ └── confirmation/
β”‚ β”‚ β”œβ”€β”€ type.ts
β”‚ β”‚ β”œβ”€β”€ schema.ts
β”‚ β”‚ β”œβ”€β”€ validators.ts
β”‚ β”‚ └── ConfirmationForm.tsx
β”‚ β”‚
β”‚ β”œβ”€β”€ sub-forms/ # Reusable sub-form modules
β”‚ β”‚ β”œβ”€β”€ address/
β”‚ β”‚ β”‚ β”œβ”€β”€ type.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ schema.ts
β”‚ β”‚ β”‚ β”œβ”€β”€ validators.ts
β”‚ β”‚ β”‚ └── AddressForm.tsx
β”‚ β”‚ β”‚
β”‚ β”‚ └── personal-data/
β”‚ β”‚ β”œβ”€β”€ type.ts
β”‚ β”‚ β”œβ”€β”€ schema.ts
β”‚ β”‚ β”œβ”€β”€ validators.ts
β”‚ β”‚ └── PersonalDataForm.tsx
β”‚ β”‚
β”‚ β”œβ”€β”€ services/ # API services
β”‚ β”‚ └── api.ts
β”‚ β”‚
β”‚ └── utils/ # Form utilities
β”‚ └── formTransformers.ts
β”‚
└── lib/ # Shared utilities

Key Principles​

1. Colocation​

Each form step and sub-form is self-contained with its own:

  • type.ts β€” TypeScript interface
  • schema.ts β€” Form schema with field configurations
  • validators.ts β€” Validation rules
  • behaviors.ts β€” Computed fields, conditional logic
  • *Form.tsx β€” React component

2. Root Aggregators​

Root-level files combine all step modules:

forms/credit-application/type.ts
// Re-export types from steps and sub-forms
export type { LoanInfoStep } from './steps/loan-info/type';
export type { PersonalInfoStep } from './steps/personal-info/type';
export type { Address } from './sub-forms/address/type';

// Main form interface
export interface CreditApplicationForm {
// Step 1: Loan Info
loanType: LoanType;
loanAmount: number;
loanTerm: number;
// ... more fields from all steps
}
forms/credit-application/schema.ts
import { loanInfoSchema } from './steps/loan-info/schema';
import { personalInfoSchema } from './steps/personal-info/schema';

export const creditApplicationSchema = {
...loanInfoSchema,
...personalInfoSchema,
// Computed fields at root level
monthlyPayment: { value: 0, disabled: true },
};

Key Files​

Step Type​

forms/credit-application/steps/loan-info/type.ts
export type LoanType = 'consumer' | 'mortgage' | 'car';

export interface LoanInfoStep {
loanType: LoanType;
loanAmount: number;
loanTerm: number;
loanPurpose: string;
// Mortgage-specific
propertyValue: number;
initialPayment: number;
// Car-specific
carBrand: string;
carModel: string;
}

Step Schema​

forms/credit-application/steps/loan-info/schema.ts
import type { FormSchema } from '@reformer/core';
import { Input, Select, Textarea } from '@/components/ui';
import type { LoanInfoStep } from './type';

export const loanInfoSchema: FormSchema<LoanInfoStep> = {
loanType: {
value: 'consumer',
component: Select,
componentProps: {
label: 'Loan Type',
options: [
{ value: 'consumer', label: 'Consumer' },
{ value: 'mortgage', label: 'Mortgage' },
{ value: 'car', label: 'Car Loan' },
],
},
},
loanAmount: {
value: null,
component: Input,
componentProps: { label: 'Loan Amount', type: 'number' },
},
// ... more fields
};

Step Validators​

forms/credit-application/steps/loan-info/validators.ts
import { required, min, max, applyWhen } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '../../type';

export const loanValidation: ValidationSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
required(path.loanType, { message: 'Select loan type' });
required(path.loanAmount, { message: 'Enter loan amount' });
min(path.loanAmount, 50000, { message: 'Minimum 50,000' });
max(path.loanAmount, 10000000, { message: 'Maximum 10,000,000' });

// Conditional validation for mortgage
applyWhen(
path.loanType,
(type) => type === 'mortgage',
(p) => {
required(p.propertyValue, { message: 'Enter property value' });
required(p.initialPayment, { message: 'Enter initial payment' });
}
);
};

Step Behaviors​

forms/credit-application/steps/loan-info/behaviors.ts
import { computeFrom, enableWhen, disableWhen } from '@reformer/core/behaviors';
import type { BehaviorSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from '../../type';

export const loanBehaviorSchema: BehaviorSchemaFn<CreditApplicationForm> = (
path: FieldPath<CreditApplicationForm>
) => {
// Show mortgage fields only for mortgage type
enableWhen(path.propertyValue, (form) => form.loanType === 'mortgage');
enableWhen(path.initialPayment, (form) => form.loanType === 'mortgage');

// Compute interest rate based on loan type
computeFrom([path.loanType], path.interestRate, (values) => {
const rates = { consumer: 15, mortgage: 10, car: 12 };
return rates[values.loanType] || 15;
});
};

Root Validators (Cross-Step)​

forms/credit-application/validators.ts
import { validate } from '@reformer/core/validators';
import type { ValidationSchemaFn, FieldPath } from '@reformer/core';
import type { CreditApplicationForm } from './type';

// Import step validators
import { loanValidation } from './steps/loan-info/validators';
import { personalValidation } from './steps/personal-info/validators';

// Cross-step validation
const crossStepValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
// Initial payment must be >= 20% of property value
validate(path.initialPayment, (value, ctx) => {
if (ctx.form.loanType.value.value !== 'mortgage') return null;
const propertyValue = ctx.form.propertyValue.value.value;
if (!propertyValue || !value) return null;
const minPayment = propertyValue * 0.2;
if (value < minPayment) {
return { code: 'minInitialPayment', message: `Minimum: ${minPayment}` };
}
return null;
});
};

// Combine all validators
export const creditApplicationValidation: ValidationSchemaFn<CreditApplicationForm> = (path) => {
loanValidation(path);
personalValidation(path);
crossStepValidation(path);
};

Main Form Component​

forms/credit-application/CreditApplicationForm.tsx
import { useMemo } from 'react';
import { createForm } from '@reformer/core';
import { creditApplicationSchema } from './schema';
import { creditApplicationBehaviors } from './behaviors';
import { creditApplicationValidation } from './validators';
import type { CreditApplicationForm as CreditApplicationFormType } from './type';

function CreditApplicationForm() {
// Create form instance with useMemo for stable reference
const form = useMemo(
() =>
createForm<CreditApplicationFormType>({
form: creditApplicationSchema,
behavior: creditApplicationBehaviors,
validation: creditApplicationValidation,
}),
[]
);

return (
// ... render form steps
);
}

Scaling: Simple vs Complex Forms​

Simple Form (Single File)​

For small forms, keep everything in one file:

forms/
└── contact/
└── ContactForm.tsx # Schema, validation, behaviors, component

Medium Form (Separated Files)​

Split into dedicated files:

forms/
└── registration/
β”œβ”€β”€ type.ts
β”œβ”€β”€ schema.ts
β”œβ”€β”€ validators.ts
β”œβ”€β”€ behaviors.ts
└── RegistrationForm.tsx

Complex Multi-Step Form (Full Colocation)​

Use the complete recommended structure:

forms/
└── credit-application/
β”œβ”€β”€ type.ts
β”œβ”€β”€ schema.ts
β”œβ”€β”€ validators.ts
β”œβ”€β”€ behaviors.ts
β”œβ”€β”€ CreditApplicationForm.tsx
β”œβ”€β”€ steps/
β”‚ β”œβ”€β”€ loan-info/
β”‚ β”œβ”€β”€ personal-info/
β”‚ β”œβ”€β”€ contact-info/
β”‚ β”œβ”€β”€ employment/
β”‚ β”œβ”€β”€ additional-info/
β”‚ └── confirmation/
β”œβ”€β”€ sub-forms/
β”‚ β”œβ”€β”€ address/
β”‚ β”œβ”€β”€ personal-data/
β”‚ β”œβ”€β”€ passport-data/
β”‚ β”œβ”€β”€ property/
β”‚ β”œβ”€β”€ existing-loan/
β”‚ └── co-borrower/
β”œβ”€β”€ services/
β”‚ └── api.ts
└── utils/
└── formTransformers.ts

Best Practices​

PracticeWhy
ColocationRelated files together, easy navigation
Group by feature, not typeFind all step files in one place
Use useMemo for formStable form instance per component
Split validators by stepValidate only current step
Root aggregatorsSingle entry point for schema/validators/behaviors
Extract sub-formsReuse address, personal data across forms

Benefits of Colocation​

  1. Discoverability β€” All related files in one folder
  2. Maintainability β€” Change one step without affecting others
  3. Refactoring β€” Move/rename entire step folders
  4. Code Splitting β€” Import only needed step validators
  5. Team Collaboration β€” Different team members work on different steps

Next Steps​