Schema Composition
Decompose and reuse schemas across your application.
Why Composition?β
- Avoid duplication β Write schemas once, use everywhere
- Consistency β Same validation rules across forms
- Maintainability β Update in one place
- Testing β Test schemas in isolation
Factory Functionsβ
Always Use Factory Functions
Use functions that return schemas, not direct objects.
// β
Good β factory function (new object each time)
export const addressSchema = (): FormSchema<Address> => ({
street: { value: '' },
city: { value: '' },
});
// β Bad β shared reference (forms share same object)
export const addressSchema = {
street: { value: '' },
city: { value: '' },
};
Reusable Field Schemasβ
Create common field configurations:
schemas/common-fields.ts
import { FieldConfig } from '@reformer/core';
export const emailField = (): FieldConfig<string> => ({
value: '',
});
export const phoneField = (): FieldConfig<string> => ({
value: '',
});
export const dateField = (): FieldConfig<Date | null> => ({
value: null,
});
export const booleanField = (defaultValue = false): FieldConfig<boolean> => ({
value: defaultValue,
});
Usage:
const form = new GroupNode({
form: {
email: emailField(),
phone: phoneField(),
birthDate: dateField(),
newsletter: booleanField(true),
},
});
Reusable Group Schemasβ
Create schemas for common data structures:
schemas/address-schema.ts
import { FormSchema } from '@reformer/core';
export interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}
export const addressSchema = (): FormSchema<Address> => ({
street: { value: '' },
city: { value: '' },
state: { value: '' },
zipCode: { value: '' },
});
schemas/person-schema.ts
import { FormSchema } from '@reformer/core';
export interface Person {
firstName: string;
lastName: string;
email: string;
}
export const personSchema = (): FormSchema<Person> => ({
firstName: { value: '' },
lastName: { value: '' },
email: { value: '' },
});
Composing schemas:
interface UserForm {
person: Person;
billingAddress: Address;
shippingAddress: Address;
}
const form = new GroupNode<UserForm>({
form: {
person: personSchema(),
billingAddress: addressSchema(),
shippingAddress: addressSchema(),
},
});
Reusable Validation Setsβ
Extract validation logic into functions:
validators/address-validators.ts
import { FieldPath } from '@reformer/core';
import { required, pattern } from '@reformer/core/validators';
import { Address } from '../schemas/address-schema';
export function validateAddress(path: FieldPath<Address>) {
required(path.street);
required(path.city);
required(path.state);
required(path.zipCode);
pattern(path.zipCode, /^\d{5}(-\d{4})?$/, 'Invalid ZIP code');
}
validators/person-validators.ts
import { FieldPath } from '@reformer/core';
import { required, email, minLength } from '@reformer/core/validators';
import { Person } from '../schemas/person-schema';
export function validatePerson(path: FieldPath<Person>) {
required(path.firstName);
minLength(path.firstName, 2);
required(path.lastName);
required(path.email);
email(path.email);
}
Usage:
const form = new GroupNode<UserForm>({
form: {
person: personSchema(),
billingAddress: addressSchema(),
shippingAddress: addressSchema(),
},
validation: (path) => {
validatePerson(path.person);
validateAddress(path.billingAddress);
validateAddress(path.shippingAddress);
},
});
Reusable Behavior Setsβ
Extract behavior logic into functions:
behaviors/address-behaviors.ts
import { FieldPath } from '@reformer/core';
import { transformValue } from '@reformer/core/behaviors';
import { Address } from '../schemas/address-schema';
export function addressBehaviors(path: FieldPath<Address>) {
// Auto-format ZIP code
transformValue(path.zipCode, (value) => {
const digits = value.replace(/\D/g, '');
if (digits.length === 9) {
return `${digits.slice(0, 5)}-${digits.slice(5)}`;
}
return value;
});
}
Usage:
const form = new GroupNode<UserForm>({
form: {
person: personSchema(),
billingAddress: addressSchema(),
shippingAddress: addressSchema(),
},
behavior: (path) => {
addressBehaviors(path.billingAddress);
addressBehaviors(path.shippingAddress);
},
});
Complete Module Patternβ
Bundle schema, validation, and behaviors together:
modules/
βββ contact-info/
βββ schema.ts # Type + form schema
βββ validators.ts # Validation rules
βββ behaviors.ts # Reactive logic
βββ index.ts # Public exports
modules/contact-info/schema.ts
import { FormSchema } from '@reformer/core';
export interface ContactInfo {
email: string;
phone: string;
preferredContact: 'email' | 'phone';
}
export const contactInfoSchema = (): FormSchema<ContactInfo> => ({
email: { value: '' },
phone: { value: '' },
preferredContact: { value: 'email' },
});
modules/contact-info/validators.ts
import { FieldPath } from '@reformer/core';
import { required, email, pattern } from '@reformer/core/validators';
import { ContactInfo } from './schema';
export function validateContactInfo(path: FieldPath<ContactInfo>) {
required(path.email);
email(path.email);
required(path.phone);
pattern(path.phone, /^\d{10}$/, 'Must be 10 digits');
}
modules/contact-info/behaviors.ts
import { FieldPath } from '@reformer/core';
import { transformValue } from '@reformer/core/behaviors';
import { ContactInfo } from './schema';
export function contactInfoBehaviors(path: FieldPath<ContactInfo>) {
transformValue(path.phone, (value) => {
const digits = value.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return value;
});
}
modules/contact-info/index.ts
export { contactInfoSchema, type ContactInfo } from './schema';
export { validateContactInfo } from './validators';
export { contactInfoBehaviors } from './behaviors';
Usage:
import {
contactInfoSchema,
validateContactInfo,
contactInfoBehaviors,
type ContactInfo,
} from './modules/contact-info';
interface MyForm {
name: string;
contactInfo: ContactInfo;
}
const form = new GroupNode<MyForm>({
form: {
name: { value: '' },
contactInfo: contactInfoSchema(),
},
validation: (path) => {
required(path.name);
validateContactInfo(path.contactInfo);
},
behavior: (path) => {
contactInfoBehaviors(path.contactInfo);
},
});
Configurable Schemasβ
Create schema factories with options:
schemas/configurable-person.ts
interface PersonSchemaOptions {
includeMiddleName?: boolean;
includePhone?: boolean;
}
export function createPersonSchema(options: PersonSchemaOptions = {}): FormSchema<Person> {
const schema: FormSchema<Person> = {
firstName: { value: '' },
lastName: { value: '' },
email: { value: '' },
};
if (options.includeMiddleName) {
schema.middleName = { value: '' };
}
if (options.includePhone) {
schema.phone = { value: '' };
}
return schema;
}
Usage:
// Basic person
const simple = createPersonSchema();
// Person with all fields
const detailed = createPersonSchema({
includeMiddleName: true,
includePhone: true,
});
Recommended Folder Structureβ
src/
βββ forms/ # Form instances
β βββ user-form.ts
β βββ order-form.ts
β
βββ schemas/ # Reusable schemas
β βββ common-fields.ts
β βββ address-schema.ts
β βββ person-schema.ts
β
βββ validators/ # Reusable validators
β βββ address-validators.ts
β βββ person-validators.ts
β
βββ behaviors/ # Reusable behaviors
β βββ address-behaviors.ts
β βββ format-behaviors.ts
β
βββ modules/ # Complete modules
βββ contact-info/
β βββ schema.ts
β βββ validators.ts
β βββ behaviors.ts
β βββ index.ts
βββ payment-info/
βββ ...
Best Practicesβ
| Practice | Why |
|---|---|
| Use factory functions | Avoid shared references |
| Export types with schemas | Better type inference |
| Bundle related schemas | Single import for module |
| Use descriptive names | validatePerson not validate1 |
| Test schemas separately | Easier debugging |
Next Stepsβ
- Project Structure β Organization tips