Project Structure
Organize your forms for scalability and maintainability using colocation β keeping related files together.
Recommended Structure (Colocation)β
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 interfaceschema.tsβ Form schema with field configurationsvalidators.tsβ Validation rulesbehaviors.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β
| Practice | Why |
|---|---|
| Colocation | Related files together, easy navigation |
| Group by feature, not type | Find all step files in one place |
| Use useMemo for form | Stable form instance per component |
| Split validators by step | Validate only current step |
| Root aggregators | Single entry point for schema/validators/behaviors |
| Extract sub-forms | Reuse address, personal data across forms |
Benefits of Colocationβ
- Discoverability β All related files in one folder
- Maintainability β Change one step without affecting others
- Refactoring β Move/rename entire step folders
- Code Splitting β Import only needed step validators
- Team Collaboration β Different team members work on different steps
Next Stepsβ
- Schema Composition β Reusable schemas and validators