@reformer/core 1.0.0 → 1.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -1,847 +1,423 @@
1
- # ReFormer
1
+ # ReFormer - LLM Integration Guide
2
2
 
3
- > Signals-based reactive form state management library for React.
4
- > Built on Preact Signals Core for fine-grained reactivity with full TypeScript support.
5
- > Key features: type-safe schemas, declarative validation, reactive behaviors, nested forms, dynamic arrays.
3
+ ## 1. QUICK REFERENCE
6
4
 
7
- ## Installation
5
+ ### Imports (CRITICALLY IMPORTANT)
8
6
 
9
- ```bash
10
- npm install reformer
11
- ```
12
-
13
- Peer dependencies:
14
- - React 18+ or React 19+
15
- - @preact/signals-core ^1.8.0
16
-
17
- ## Quick Start
18
-
19
- ```tsx
20
- import { createForm } from 'reformer';
21
- import { required, email, minLength } from 'reformer/validators';
22
- import { useFormControl } from 'reformer';
7
+ | What | Where |
8
+ | ------------------------------------------------------------------------------------------- | --------------------------- |
9
+ | `ValidationSchemaFn`, `BehaviorSchemaFn`, `FieldPath`, `GroupNodeWithControls`, `FieldNode` | `@reformer/core` |
10
+ | `required`, `min`, `max`, `minLength`, `email`, `validate`, `validateTree` | `@reformer/core/validators` |
11
+ | `computeFrom`, `enableWhen`, `disableWhen`, `copyFrom`, `watchField` | `@reformer/core/behaviors` |
23
12
 
24
- // 1. Define your form type
25
- type ContactForm = {
26
- name: string;
27
- email: string;
28
- message: string;
29
- };
13
+ ### Type Values
30
14
 
31
- // 2. Create form with schema and validation
32
- const form = createForm<ContactForm>({
33
- form: {
34
- name: { value: '', component: Input, componentProps: { label: 'Name' } },
35
- email: { value: '', component: Input, componentProps: { label: 'Email' } },
36
- message: { value: '', component: Textarea, componentProps: { label: 'Message' } },
37
- },
38
- validation: (path) => {
39
- required(path.name);
40
- minLength(path.name, 2);
41
- required(path.email);
42
- email(path.email);
43
- required(path.message);
44
- },
45
- });
46
-
47
- // 3. Use in React component
48
- function ContactForm() {
49
- const { value, errors, shouldShowError } = useFormControl(form.name);
50
-
51
- return (
52
- <input
53
- value={value}
54
- onChange={(e) => form.name.setValue(e.target.value)}
55
- onBlur={() => form.name.markAsTouched()}
56
- />
57
- );
58
- }
59
- ```
15
+ - Optional numbers: `number | undefined` (NOT `null`)
16
+ - Optional strings: `string` (empty string by default)
17
+ - Do NOT add `[key: string]: unknown` to form interfaces
60
18
 
61
- ## Architecture
19
+ ## 2. API SIGNATURES
62
20
 
63
- ### Node Hierarchy
64
-
65
- ReFormer uses a tree-based node architecture:
66
-
67
- ```
68
- GroupNode (Form)
69
- ├── FieldNode (single values: string, number, boolean)
70
- ├── GroupNode (nested objects)
71
- └── ArrayNode (dynamic arrays)
72
- ```
73
-
74
- All nodes inherit from abstract `FormNode` base class.
75
-
76
- ### Key Concepts
77
-
78
- 1. **Form Schema** - Defines structure, components, and initial values
79
- 2. **Validation Schema** - Declares validation rules (separate from structure)
80
- 3. **Behavior Schema** - Reactive logic (computed fields, conditional visibility, sync)
81
-
82
- ### Signals-based Reactivity
83
-
84
- - Uses @preact/signals-core for fine-grained reactivity
85
- - Only affected components re-render when values change
86
- - React integration via useSyncExternalStore
87
-
88
- ## Form Schema
89
-
90
- ### FieldConfig<T>
21
+ ### Validators
91
22
 
92
23
  ```typescript
93
- interface FieldConfig<T> {
94
- value: T | null; // Initial value
95
- component: ComponentType<any>; // React component to render
96
- componentProps?: Record<string, any>; // Props passed to component
97
- validators?: ValidatorFn<T>[]; // Sync validators
98
- asyncValidators?: AsyncValidatorFn<T>[]; // Async validators
99
- disabled?: boolean; // Initially disabled
100
- updateOn?: 'change' | 'blur' | 'submit'; // When to validate (default: 'change')
101
- debounce?: number; // Debounce validation in ms
102
- }
24
+ required(path, options?: { message?: string })
25
+ min(path, value: number, options?: { message?: string })
26
+ max(path, value: number, options?: { message?: string })
27
+ minLength(path, length: number, options?: { message?: string })
28
+ maxLength(path, length: number, options?: { message?: string })
29
+ email(path, options?: { message?: string })
30
+ validate(path, validator: (value) => ValidationError | null)
31
+ validateTree(validator: (ctx) => ValidationError | null)
32
+ applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
103
33
  ```
104
34
 
105
- ### ArrayConfig<T>
106
-
107
- Arrays use single-element tuple syntax in schema:
108
-
109
- ```typescript
110
- interface FormType {
111
- phones: { type: string; number: string }[];
112
- }
113
-
114
- const schema: FormSchema<FormType> = {
115
- phones: [{
116
- type: { value: 'mobile', component: Select },
117
- number: { value: '', component: Input },
118
- }],
119
- };
120
- ```
121
-
122
- ### Complete Schema Example
35
+ ### Behaviors
123
36
 
124
37
  ```typescript
125
- import { createForm } from 'reformer';
126
-
127
- type UserForm = {
128
- name: string;
129
- email: string;
130
- age: number;
131
- address: {
132
- street: string;
133
- city: string;
134
- };
135
- phones: { type: string; number: string }[];
136
- };
38
+ enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
39
+ disableWhen(path, condition: (form) => boolean)
40
+ computeFrom(sourcePaths[], targetPath, compute: (values) => result)
41
+ watchField(path, callback: (value, ctx: BehaviorContext) => void)
42
+ copyFrom(sourcePath, targetPath, options?: { when?, fields?, transform? })
137
43
 
138
- const form = createForm<UserForm>({
139
- form: {
140
- name: { value: '', component: Input },
141
- email: { value: '', component: Input },
142
- age: { value: 0, component: Input, componentProps: { type: 'number' } },
143
- address: {
144
- street: { value: '', component: Input },
145
- city: { value: '', component: Input },
146
- },
147
- phones: [{
148
- type: { value: 'mobile', component: Select },
149
- number: { value: '', component: Input },
150
- }],
151
- },
152
- validation: (path) => { /* validators */ },
153
- behavior: (path) => { /* behaviors */ },
154
- });
44
+ // BehaviorContext interface:
45
+ interface BehaviorContext<TForm> {
46
+ form: TForm; // Current form state
47
+ setFieldValue: (path, value) => void; // Set field value
48
+ getFieldValue: (path) => unknown; // Get field value
49
+ }
155
50
  ```
156
51
 
157
- ## Node Types
158
-
159
- ### FieldNode<T>
160
-
161
- Represents a single form field value.
162
-
163
- **Properties (all are Signals):**
164
- - `value` - Current value
165
- - `valid` / `invalid` - Validation state
166
- - `touched` / `untouched` - User interaction state
167
- - `dirty` / `pristine` - Value changed from initial
168
- - `errors` - Array of ValidationError objects
169
- - `shouldShowError` - true when invalid AND (touched OR dirty)
170
- - `disabled` - Is field disabled
171
- - `pending` - Async validation in progress
172
- - `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
173
- - `componentProps` - Props for component
174
-
175
- **Methods:**
176
- - `setValue(value, options?)` - Set new value
177
- - `reset()` - Reset to initial value
178
- - `markAsTouched()` - Mark as touched
179
- - `markAsDirty()` - Mark as dirty
180
- - `disable()` / `enable()` - Toggle disabled state
181
- - `validate()` - Run validation
182
- - `getErrors(filter?)` - Get filtered errors
183
-
184
- ### GroupNode<T>
52
+ ## 3. COMMON PATTERNS
185
53
 
186
- Groups multiple fields into an object.
54
+ ### Conditional Fields with Auto-Reset
187
55
 
188
- **Properties:**
189
- - `controls` - Dictionary of child nodes
190
- - All FormNode properties (computed from children)
191
-
192
- **Methods:**
193
- - `getFieldByPath(path: string)` - Get field by dot-notation path
194
- - `patchValue(partial)` - Update subset of fields
195
- - `resetAll()` - Reset all children
196
- - All FormNode methods
197
-
198
- **Proxy Access:**
199
56
  ```typescript
200
- // Type-safe field access via proxy
201
- form.name // FieldNode<string>
202
- form.address.city // FieldNode<string>
203
- form.phones // ArrayNode
57
+ enableWhen(path.mortgageFields, (form) => form.loanType === 'mortgage', {
58
+ resetOnDisable: true,
59
+ });
204
60
  ```
205
61
 
206
- ### ArrayNode<T>
207
-
208
- Manages dynamic arrays.
209
-
210
- **Properties:**
211
- - `controls` - Array of GroupNode items
212
- - `length` - Number of items
213
-
214
- **Methods:**
215
- - `push(value)` - Add item to end
216
- - `insert(index, value)` - Insert at position
217
- - `removeAt(index)` - Remove at position
218
- - `move(from, to)` - Move item
219
- - `clear()` - Remove all items
220
- - `at(index)` - Get item at index
62
+ ### Computed Field from Nested to Root Level
221
63
 
222
64
  ```typescript
223
- // Usage
224
- form.phones.push({ type: 'work', number: '' });
225
- form.phones.removeAt(0);
226
- form.phones.at(0).controls.number.setValue('123-456');
65
+ // DO NOT use computeFrom for cross-level computations
66
+ // Use watchField instead:
67
+ watchField(path.nested.field, (value, ctx) => {
68
+ ctx.setFieldValue('rootField', computedValue);
69
+ });
227
70
  ```
228
71
 
229
- ## Validation
230
-
231
- ### ValidationSchemaFn
72
+ ### Type-Safe useFormControl
232
73
 
233
74
  ```typescript
234
- import { required, email, minLength, validate, validateAsync } from 'reformer/validators';
235
-
236
- const form = createForm<FormType>({
237
- form: { /* schema */ },
238
- validation: (path) => {
239
- // Built-in validators
240
- required(path.name);
241
- email(path.email);
242
- minLength(path.password, 8);
243
-
244
- // Custom validator
245
- validate(path.age, (value) => {
246
- if (value < 18) return { code: 'tooYoung', message: 'Must be 18+' };
247
- return null;
248
- });
249
-
250
- // Async validator
251
- validateAsync(path.username, async (value) => {
252
- const available = await checkUsername(value);
253
- if (!available) return { code: 'taken', message: 'Username taken' };
254
- return null;
255
- }, { debounce: 500 });
256
- },
257
- });
75
+ const { value } = useFormControl(form.field as FieldNode<ExpectedType>);
258
76
  ```
259
77
 
260
- ### Built-in Validators
78
+ ## 4. ⚠️ COMMON MISTAKES
261
79
 
262
- All imported from `reformer/validators`:
263
-
264
- | Validator | Usage | Description |
265
- |-----------|-------|-------------|
266
- | `required(path)` | `required(path.name)` | Non-empty value |
267
- | `email(path)` | `email(path.email)` | Valid email format |
268
- | `minLength(path, n)` | `minLength(path.name, 2)` | Minimum string length |
269
- | `maxLength(path, n)` | `maxLength(path.bio, 500)` | Maximum string length |
270
- | `min(path, n)` | `min(path.age, 18)` | Minimum number value |
271
- | `max(path, n)` | `max(path.qty, 100)` | Maximum number value |
272
- | `pattern(path, regex)` | `pattern(path.code, /^[A-Z]+$/)` | Match regex |
273
- | `url(path)` | `url(path.website)` | Valid URL |
274
- | `phone(path)` | `phone(path.phone)` | Valid phone |
275
- | `number(path)` | `number(path.amount)` | Must be number |
276
- | `date(path)` | `date(path.birthDate)` | Valid date |
277
-
278
- ### Custom Validator Example
80
+ ### Validators
279
81
 
280
82
  ```typescript
281
- // validators/password.ts
282
- export function strongPassword() {
283
- return (value: string) => {
284
- if (!value) return null; // Skip empty (use required() separately)
285
-
286
- const errors: string[] = [];
287
- if (!/[A-Z]/.test(value)) errors.push('uppercase');
288
- if (!/[a-z]/.test(value)) errors.push('lowercase');
289
- if (!/[0-9]/.test(value)) errors.push('number');
290
- if (value.length < 8) errors.push('length');
291
-
292
- if (errors.length) {
293
- return { code: 'weakPassword', message: 'Password too weak', params: { missing: errors } };
294
- }
295
- return null;
296
- };
297
- }
83
+ // ❌ WRONG
84
+ required(path.email, 'Email is required');
298
85
 
299
- // Usage
300
- validation: (path) => {
301
- required(path.password);
302
- validate(path.password, strongPassword());
303
- }
86
+ // ✅ CORRECT
87
+ required(path.email, { message: 'Email is required' });
304
88
  ```
305
89
 
306
- ### Async Validation Example
90
+ ### Types
307
91
 
308
92
  ```typescript
309
- // Check username availability on server
310
- validation: (path) => {
311
- required(path.username);
93
+ // WRONG
94
+ amount: number | null;
95
+ [key: string]: unknown;
312
96
 
313
- validateAsync(path.username, async (value, ctx) => {
314
- if (!value || value.length < 3) return null;
315
-
316
- const response = await fetch(`/api/check-username?u=${value}`);
317
- const { available } = await response.json();
318
-
319
- if (!available) {
320
- return { code: 'usernameTaken', message: 'Username is already taken' };
321
- }
322
- return null;
323
- }, { debounce: 500 });
324
- }
97
+ // CORRECT
98
+ amount: number | undefined;
99
+ // No index signature
325
100
  ```
326
101
 
327
- ### Cross-field Validation
328
-
329
- ```typescript
330
- import { validateTree } from 'reformer/validators';
331
-
332
- validation: (path) => {
333
- required(path.password);
334
- required(path.confirmPassword);
335
-
336
- // Cross-field validation
337
- validateTree((ctx) => {
338
- const password = ctx.form.password.value.value;
339
- const confirm = ctx.form.confirmPassword.value.value;
340
-
341
- if (password && confirm && password !== confirm) {
342
- return {
343
- code: 'passwordMismatch',
344
- message: 'Passwords do not match',
345
- path: 'confirmPassword',
346
- };
347
- }
348
- return null;
349
- });
350
- }
351
- ```
352
-
353
- ## Behaviors
354
-
355
- Behaviors add reactive logic to forms. All imported from `reformer/behaviors`.
356
-
357
102
  ### computeFrom
358
103
 
359
- Calculate field value from other fields:
360
-
361
104
  ```typescript
362
- import { computeFrom } from 'reformer/behaviors';
363
-
364
- behavior: (path) => {
365
- // total = price * quantity
366
- computeFrom(
367
- [path.price, path.quantity], // Watch these fields
368
- path.total, // Update this field
369
- ({ price, quantity }) => price * quantity // Compute function
370
- );
371
- }
372
- ```
105
+ // WRONG - different nesting levels
106
+ computeFrom([path.nested.a, path.nested.b], path.root, ...)
373
107
 
374
- ### enableWhen / disableWhen
108
+ // CORRECT - use watchField
109
+ watchField(path.nested.a, (_, ctx) => {
110
+ ctx.setFieldValue('root', computed);
111
+ });
112
+ ```
375
113
 
376
- Conditional field enable/disable:
114
+ ### Imports
377
115
 
378
116
  ```typescript
379
- import { enableWhen, disableWhen } from 'reformer/behaviors';
380
-
381
- behavior: (path) => {
382
- // Enable discount field only when total > 500
383
- enableWhen(path.discount, (form) => form.total > 500);
117
+ // WRONG - types are not in submodules
118
+ import { ValidationSchemaFn } from '@reformer/core/validators';
384
119
 
385
- // Disable shipping when pickup is selected
386
- disableWhen(path.shippingAddress, (form) => form.deliveryMethod === 'pickup');
387
- }
120
+ // CORRECT - types from main module
121
+ import type { ValidationSchemaFn } from '@reformer/core';
122
+ import { required, email } from '@reformer/core/validators';
388
123
  ```
389
124
 
390
- ### watchField
391
-
392
- React to field changes with custom logic:
393
-
394
- ```typescript
395
- import { watchField } from 'reformer/behaviors';
396
-
397
- behavior: (path) => {
398
- // Load cities when country changes
399
- watchField(path.country, async (value, ctx) => {
400
- const cities = await fetchCities(value);
401
- ctx.form.city.updateComponentProps({ options: cities });
402
- ctx.form.city.setValue(''); // Reset city selection
403
- }, { debounce: 300 });
404
- }
405
- ```
125
+ ## 5. TROUBLESHOOTING
406
126
 
407
- ### copyFrom
127
+ | Error | Cause | Solution |
128
+ | ------------------------------------------------------ | ------------------------------ | --------------------------------- |
129
+ | `'string' is not assignable to '{ message?: string }'` | Wrong validator format | Use `{ message: 'text' }` |
130
+ | `'null' is not assignable to 'undefined'` | Wrong optional type | Replace `null` with `undefined` |
131
+ | `FormFields[]` instead of concrete type | Type inference issue | Use `as FieldNode<T>` |
132
+ | `Type 'X' is missing properties from type 'Y'` | Cross-level computeFrom | Use watchField instead |
133
+ | `Module has no exported member` | Wrong import source | Types from core, functions from submodules |
408
134
 
409
- Copy values from one field/group to another:
135
+ ## 6. COMPLETE IMPORT EXAMPLE
410
136
 
411
137
  ```typescript
412
- import { copyFrom } from 'reformer/behaviors';
413
-
414
- behavior: (path) => {
415
- // Copy billing address to shipping when checkbox is checked
416
- copyFrom(path.billingAddress, path.shippingAddress, {
417
- when: (form) => form.sameAsShipping === true,
418
- fields: 'all', // or ['street', 'city', 'zip']
419
- });
420
- }
421
- ```
422
-
423
- ### syncFields
138
+ // Types - always from @reformer/core
139
+ import type {
140
+ ValidationSchemaFn,
141
+ BehaviorSchemaFn,
142
+ FieldPath,
143
+ GroupNodeWithControls,
144
+ FieldNode,
145
+ } from '@reformer/core';
424
146
 
425
- Two-way field synchronization:
147
+ // Core functions
148
+ import { createForm, useFormControl } from '@reformer/core';
426
149
 
427
- ```typescript
428
- import { syncFields } from 'reformer/behaviors';
150
+ // Validators - from /validators submodule
151
+ import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
429
152
 
430
- behavior: (path) => {
431
- syncFields(path.field1, path.field2);
432
- }
153
+ // Behaviors - from /behaviors submodule
154
+ import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
433
155
  ```
434
156
 
435
- ### resetWhen
436
-
437
- Reset field when condition is met:
157
+ ## 7. FORM TYPE DEFINITION
438
158
 
439
159
  ```typescript
440
- import { resetWhen } from 'reformer/behaviors';
441
-
442
- behavior: (path) => {
443
- // Reset city when country changes
444
- resetWhen(path.city, [path.country]);
445
- }
446
- ```
160
+ // CORRECT form type definition
161
+ interface MyForm {
162
+ // Required fields
163
+ name: string;
164
+ email: string;
447
165
 
448
- ### revalidateWhen
166
+ // Optional fields - use undefined, not null
167
+ phone?: string;
168
+ age?: number;
449
169
 
450
- Trigger revalidation when another field changes:
170
+ // Enum/union types
171
+ status: 'active' | 'inactive';
451
172
 
452
- ```typescript
453
- import { revalidateWhen } from 'reformer/behaviors';
173
+ // Nested objects
174
+ address: {
175
+ street: string;
176
+ city: string;
177
+ };
454
178
 
455
- behavior: (path) => {
456
- // Revalidate confirmPassword when password changes
457
- revalidateWhen(path.confirmPassword, [path.password]);
179
+ // Arrays - use tuple format for schema
180
+ items: Array<{
181
+ id: string;
182
+ name: string;
183
+ }>;
458
184
  }
459
185
  ```
460
186
 
461
- ### Custom Behavior
462
-
463
- Create reusable custom behaviors:
187
+ ## 8. CREATEFORM API
464
188
 
465
189
  ```typescript
466
- // behaviors/auto-save.ts
467
- import { Behavior } from 'reformer';
468
-
469
- interface AutoSaveOptions {
470
- debounce?: number;
471
- onSave: (data: any) => Promise<void>;
472
- }
473
-
474
- export function autoSave<T>(options: AutoSaveOptions): Behavior<T> {
475
- const { debounce = 1000, onSave } = options;
476
- let timeoutId: NodeJS.Timeout;
477
-
478
- return {
479
- key: 'autoSave',
480
- paths: [], // Empty = listen to all fields
481
- run: (values, ctx) => {
482
- clearTimeout(timeoutId);
483
- timeoutId = setTimeout(async () => {
484
- await onSave(ctx.form.getValue());
485
- }, debounce);
486
- },
487
- cleanup: () => clearTimeout(timeoutId),
488
- };
489
- }
490
-
491
- // Usage
492
- behaviors: (path, { use }) => [
493
- use(autoSave({
494
- debounce: 2000,
495
- onSave: async (data) => {
496
- await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
497
- },
498
- })),
499
- ];
500
- ```
501
-
502
- ## Recommended Project Structure
190
+ // Full config with behavior and validation
191
+ const form = createForm<MyForm>({
192
+ form: formSchema, // Required: form schema
193
+ behavior: behaviorSchema, // Optional: behavior rules
194
+ validation: validationSchema, // Optional: validation rules
195
+ });
503
196
 
504
- ### Form Organization (Colocation)
197
+ // Legacy format (schema only)
198
+ const form = createForm<MyForm>(formSchema);
505
199
 
506
- ```
507
- src/forms/
508
- ├── user-profile/
509
- │ ├── UserProfileForm.tsx # React component
510
- │ ├── type.ts # TypeScript interfaces
511
- │ ├── schema.ts # Form schema (createForm)
512
- │ ├── validators.ts # Validation rules
513
- │ ├── behaviors.ts # Reactive behaviors
514
- │ └── sub-forms/ # Reusable nested schemas
515
- │ ├── address/
516
- │ │ ├── type.ts
517
- │ │ ├── schema.ts
518
- │ │ ├── validators.ts
519
- │ │ └── AddressForm.tsx
200
+ // Form schema example
201
+ const formSchema: FormSchema<MyForm> = {
202
+ name: '',
203
+ email: '',
204
+ address: {
205
+ street: '',
206
+ city: '',
207
+ },
208
+ // Arrays use tuple format
209
+ items: [{ id: '', name: '' }] as [{ id: string; name: string }],
210
+ };
520
211
  ```
521
212
 
522
- ### Schema File Pattern
213
+ ## 9. ARRAY SCHEMA FORMAT
523
214
 
524
215
  ```typescript
525
- // forms/user-profile/schema.ts
526
- import { createForm } from 'reformer';
527
- import { validation } from './validators';
528
- import { behavior } from './behaviors';
529
- import type { UserProfile } from './type';
530
-
531
- export const createUserProfileForm = (initial?: Partial<UserProfile>) =>
532
- createForm<UserProfile>({
533
- form: {
534
- name: { value: initial?.name ?? '', component: Input },
535
- email: { value: initial?.email ?? '', component: Input },
536
- // ...
537
- },
538
- validation,
539
- behavior,
540
- });
541
- ```
542
-
543
- ### Multi-step Form Structure
544
-
545
- ```
546
- src/forms/checkout/
547
- ├── CheckoutForm.tsx # Main form component
548
- ├── type.ts # Combined type
549
- ├── schema.ts # Combined schema
550
- ├── validators.ts # Combined + cross-step validators
551
- ├── behaviors.ts # Combined + cross-step behaviors
552
- ├── steps/
553
- │ ├── shipping/
554
- │ │ ├── type.ts
555
- │ │ ├── schema.ts
556
- │ │ ├── validators.ts
557
- │ │ └── ShippingStep.tsx
558
- │ ├── payment/
559
- │ └── confirmation/
560
- └── hooks/
561
- └── useCheckoutNavigation.ts
562
- ```
563
-
564
- ## React Integration
565
-
566
- ### useFormControl<T>
567
-
568
- Subscribe to all field state changes:
216
+ // ✅ CORRECT - use tuple format for arrays
217
+ const schema = {
218
+ items: [itemSchema] as [typeof itemSchema],
219
+ properties: [propertySchema] as [typeof propertySchema],
220
+ };
569
221
 
570
- ```typescript
571
- import { useFormControl } from 'reformer';
572
-
573
- function TextField({ field }: { field: FieldNode<string> }) {
574
- const {
575
- value, // Current value
576
- valid, // Is valid
577
- invalid, // Has errors
578
- errors, // ValidationError[]
579
- touched, // User interacted
580
- disabled, // Is disabled
581
- pending, // Async validation running
582
- shouldShowError, // Show error (touched && invalid)
583
- componentProps, // Custom props from schema
584
- } = useFormControl(field);
585
-
586
- return (
587
- <div>
588
- <input
589
- value={value}
590
- onChange={(e) => field.setValue(e.target.value)}
591
- onBlur={() => field.markAsTouched()}
592
- disabled={disabled}
593
- />
594
- {shouldShowError && errors[0] && (
595
- <span className="error">{errors[0].message}</span>
596
- )}
597
- </div>
598
- );
599
- }
222
+ // ❌ WRONG - object format is NOT supported
223
+ const schema = {
224
+ items: { schema: itemSchema, initialItems: [] }, // This will NOT work
225
+ };
600
226
  ```
601
227
 
602
- ### useFormControlValue<T>
603
-
604
- Lightweight hook - returns only value (better performance):
228
+ ## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
605
229
 
606
230
  ```typescript
607
- import { useFormControlValue } from 'reformer';
231
+ // CORRECT - async watchField with ALL safeguards
232
+ watchField(
233
+ path.parentField,
234
+ async (value, ctx) => {
235
+ if (!value) return; // Guard clause
608
236
 
609
- function ConditionalField({ trigger, field }) {
610
- // Re-renders only when trigger value changes
611
- const showField = useFormControlValue(trigger);
237
+ try {
238
+ const { data } = await fetchData(value);
239
+ ctx.form.dependentField.updateComponentProps({ options: data });
240
+ } catch (error) {
241
+ console.error('Failed:', error);
242
+ ctx.form.dependentField.updateComponentProps({ options: [] });
243
+ }
244
+ },
245
+ { immediate: false, debounce: 300 } // REQUIRED options
246
+ );
612
247
 
613
- if (showField !== 'yes') return null;
614
- return <TextField field={field} />;
615
- }
248
+ // WRONG - missing safeguards
249
+ watchField(path.field, async (value, ctx) => {
250
+ const { data } = await fetchData(value); // Will fail silently!
251
+ });
616
252
  ```
617
253
 
618
- ### Performance Notes
619
-
620
- - Uses `useSyncExternalStore` for React 18+ integration
621
- - Fine-grained updates - only affected components re-render
622
- - Memoized state objects prevent unnecessary re-renders
623
- - Use `useFormControlValue` when you only need the value
624
-
625
- ## API Reference
254
+ ### Required Options for async watchField:
255
+ - `immediate: false` - prevents execution during initialization
256
+ - `debounce: 300` - prevents excessive API calls (300-500ms recommended)
257
+ - Guard clause - skip if value is empty
258
+ - try-catch - handle errors explicitly
626
259
 
627
- ### createForm<T>(config)
628
-
629
- Creates a new form instance with type-safe proxy access.
260
+ ## 11. ARRAY CLEANUP PATTERN
630
261
 
631
262
  ```typescript
632
- function createForm<T>(config: GroupNodeConfig<T>): GroupNodeWithControls<T>
263
+ // CORRECT - cleanup array when checkbox unchecked
264
+ watchField(
265
+ path.hasItems,
266
+ (hasItems, ctx) => {
267
+ if (!hasItems && ctx.form.items) {
268
+ ctx.form.items.clear();
269
+ }
270
+ },
271
+ { immediate: false }
272
+ );
633
273
 
634
- interface GroupNodeConfig<T> {
635
- form: FormSchema<T>;
636
- validation?: ValidationSchemaFn<T>;
637
- behavior?: BehaviorSchemaFn<T>;
638
- }
274
+ // WRONG - no immediate: false, no null check
275
+ watchField(path.hasItems, (hasItems, ctx) => {
276
+ if (!hasItems) ctx.form.items.clear(); // May crash on init!
277
+ });
639
278
  ```
640
279
 
641
- ### Node Common Properties
642
-
643
- All nodes have these Signal properties:
644
- - `value` - Current value
645
- - `valid` / `invalid` - Validation state
646
- - `touched` / `untouched` - Interaction state
647
- - `dirty` / `pristine` - Changed state
648
- - `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
649
- - `disabled` - Is disabled
650
- - `pending` - Async validation in progress
651
-
652
- ### Node Common Methods
280
+ ## 12. MULTI-STEP FORM VALIDATION
653
281
 
654
282
  ```typescript
655
- setValue(value: T, options?: SetValueOptions): void
656
- reset(): void
657
- markAsTouched(): void
658
- markAsDirty(): void
659
- disable(): void
660
- enable(): void
661
- validate(): Promise<void>
662
- getErrors(filter?: (error: ValidationError) => boolean): ValidationError[]
663
- ```
664
-
665
- ### SetValueOptions
283
+ // Step-specific validation schemas
284
+ const step1Validation: ValidationSchemaFn<Form> = (path) => {
285
+ required(path.loanType);
286
+ required(path.loanAmount);
287
+ };
666
288
 
667
- ```typescript
668
- interface SetValueOptions {
669
- emitEvent?: boolean; // Trigger change events (default: true)
670
- onlySelf?: boolean; // Don't propagate to parent (default: false)
671
- }
672
- ```
289
+ const step2Validation: ValidationSchemaFn<Form> = (path) => {
290
+ required(path.personalData.firstName);
291
+ required(path.personalData.lastName);
292
+ };
673
293
 
674
- ### ValidationError
294
+ // STEP_VALIDATIONS map for useStepForm hook
295
+ export const STEP_VALIDATIONS = {
296
+ 1: step1Validation,
297
+ 2: step2Validation,
298
+ };
675
299
 
676
- ```typescript
677
- interface ValidationError {
678
- code: string; // Error identifier
679
- message: string; // Human-readable message
680
- params?: Record<string, any>; // Additional error data
681
- severity?: 'error' | 'warning'; // Severity level
682
- path?: string; // Field path (for cross-field)
683
- }
300
+ // Full validation (combines all steps)
301
+ export const fullValidation: ValidationSchemaFn<Form> = (path) => {
302
+ step1Validation(path);
303
+ step2Validation(path);
304
+ };
684
305
  ```
685
306
 
686
- ### FieldStatus
687
-
688
- ```typescript
689
- type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
690
- ```
307
+ ## 13. ⚠️ EXTENDED COMMON MISTAKES
691
308
 
692
- ### Type Guards
309
+ ### Behavior Composition (Cycle Error)
693
310
 
694
311
  ```typescript
695
- import { isFieldNode, isGroupNode, isArrayNode, getNodeType } from 'reformer';
312
+ // WRONG - apply() in behavior causes "Cycle detected"
313
+ const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
314
+ apply(addressBehavior, path.address); // WILL FAIL!
315
+ };
696
316
 
697
- if (isFieldNode(node)) { /* node is FieldNode */ }
698
- if (isGroupNode(node)) { /* node is GroupNode */ }
699
- if (isArrayNode(node)) { /* node is ArrayNode */ }
700
- const type = getNodeType(node); // 'field' | 'group' | 'array'
701
- ```
317
+ // CORRECT - inline or use setup function
318
+ const setupAddressBehavior = (path: FieldPath<Address>) => {
319
+ watchField(path.region, async (region, ctx) => {
320
+ // ...
321
+ }, { immediate: false });
322
+ };
702
323
 
703
- ## Common Patterns
324
+ const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
325
+ setupAddressBehavior(path.address); // Works!
326
+ };
327
+ ```
704
328
 
705
- ### Multi-step Form
329
+ ### Infinite Loop in watchField
706
330
 
707
331
  ```typescript
708
- function MultiStepForm() {
709
- const form = useMemo(() => createCheckoutForm(), []);
710
- const [step, setStep] = useState(0);
711
-
712
- const validateStep = async () => {
713
- const stepFields = getStepFields(step);
714
- stepFields.forEach(f => f.markAsTouched());
715
- await form.validate();
716
- return stepFields.every(f => f.valid.value);
717
- };
718
-
719
- const handleNext = async () => {
720
- if (await validateStep()) {
721
- setStep(s => s + 1);
722
- }
723
- };
332
+ // WRONG - causes infinite loop
333
+ watchField(path.field, (value, ctx) => {
334
+ ctx.form.field.setValue(value.toUpperCase()); // Loop!
335
+ });
724
336
 
725
- return (
726
- <div>
727
- {step === 0 && <ShippingStep form={form} />}
728
- {step === 1 && <PaymentStep form={form} />}
729
- {step === 2 && <ConfirmationStep form={form} />}
730
-
731
- <button onClick={() => setStep(s => s - 1)} disabled={step === 0}>
732
- Back
733
- </button>
734
- <button onClick={handleNext}>
735
- {step === 2 ? 'Submit' : 'Next'}
736
- </button>
737
- </div>
738
- );
739
- }
337
+ // ✅ CORRECT - write to different field OR add guard
338
+ watchField(path.input, (value, ctx) => {
339
+ const upper = value?.toUpperCase() || '';
340
+ if (ctx.form.display.value.value !== upper) {
341
+ ctx.form.display.setValue(upper);
342
+ }
343
+ }, { immediate: false });
740
344
  ```
741
345
 
742
- ### Nested Form with Reusable Schema
346
+ ### validateTree Typing
743
347
 
744
348
  ```typescript
745
- // sub-forms/address/schema.ts
746
- export const addressSchema = {
747
- street: { value: '', component: Input, componentProps: { label: 'Street' } },
748
- city: { value: '', component: Input, componentProps: { label: 'City' } },
749
- zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
750
- };
349
+ // ❌ WRONG - implicit any
350
+ validateTree((ctx) => { ... });
751
351
 
752
- // main form
753
- const form = createForm<OrderForm>({
754
- form: {
755
- billingAddress: addressSchema,
756
- shippingAddress: addressSchema,
757
- },
352
+ // CORRECT - explicit typing
353
+ validateTree((ctx: { form: MyForm }) => {
354
+ if (ctx.form.field1 > ctx.form.field2) {
355
+ return { code: 'error', message: 'Invalid' };
356
+ }
357
+ return null;
758
358
  });
759
359
  ```
760
360
 
761
- ### Dynamic Array (Add/Remove Items)
762
-
763
- ```typescript
764
- function PhoneList({ array }: { array: ArrayNode<Phone> }) {
765
- const { length } = useFormControl(array);
766
-
767
- return (
768
- <div>
769
- {array.controls.map((phone, index) => (
770
- <div key={phone.id}>
771
- <FormField field={phone.controls.type} />
772
- <FormField field={phone.controls.number} />
773
- <button onClick={() => array.removeAt(index)}>Remove</button>
774
- </div>
775
- ))}
776
-
777
- <button onClick={() => array.push({ type: 'mobile', number: '' })}>
778
- Add Phone
779
- </button>
780
- </div>
781
- );
782
- }
783
- ```
784
-
785
- ### Conditional Fields
786
-
787
- ```typescript
788
- behavior: (path) => {
789
- // Show company fields only for business accounts
790
- enableWhen(path.companyName, (form) => form.accountType === 'business');
791
- enableWhen(path.taxId, (form) => form.accountType === 'business');
792
-
793
- // Reset company fields when switching to personal
794
- resetWhen(path.companyName, [path.accountType]);
795
- resetWhen(path.taxId, [path.accountType]);
361
+ ## 14. PROJECT STRUCTURE (COLOCATION)
362
+
363
+ ```
364
+ src/
365
+ ├── components/ui/ # Reusable UI components
366
+ │ ├── FormField.tsx
367
+ │ └── FormArrayManager.tsx
368
+
369
+ ├── forms/
370
+ │ └── [form-name]/ # Form module
371
+ │ ├── type.ts # Main form type
372
+ │ ├── schema.ts # Main schema
373
+ │ ├── validators.ts # Validators
374
+ │ ├── behaviors.ts # Behaviors
375
+ │ ├── [FormName]Form.tsx # Main component
376
+ │ │
377
+ │ ├── steps/ # Multi-step wizard
378
+ │ │ ├── loan-info/
379
+ │ │ │ ├── type.ts
380
+ │ │ │ ├── schema.ts
381
+ │ │ │ ├── validators.ts
382
+ │ │ │ ├── behaviors.ts
383
+ │ │ │ └── LoanInfoForm.tsx
384
+ │ │ └── ...
385
+ │ │
386
+ │ └── sub-forms/ # Reusable sub-forms
387
+ │ ├── address/
388
+ │ └── personal-data/
389
+ ```
390
+
391
+ ### Key Files
392
+
393
+ ```typescript
394
+ // forms/credit-application/type.ts
395
+ export type { LoanInfoStep } from './steps/loan-info/type';
396
+ export interface CreditApplicationForm {
397
+ loanType: LoanType;
398
+ loanAmount: number;
399
+ // ...
796
400
  }
797
- ```
798
-
799
- ## Troubleshooting / FAQ
800
401
 
801
- ### Q: Field not updating in React?
802
- A: Ensure you're using `useFormControl()` hook to subscribe to changes. Direct signal access (`.value.value`) won't trigger re-renders.
803
-
804
- ### Q: Validation not triggering?
805
- A: Check `updateOn` option in field config. Default is 'change'. For blur-triggered validation use `updateOn: 'blur'`.
806
-
807
- ### Q: How to access nested field by path string?
808
- A: Use `form.getFieldByPath('address.city')` for dynamic string-based access. For type-safe access use proxy: `form.address.city`.
809
-
810
- ### Q: TypeScript errors with schema?
811
- A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
812
-
813
- ### Q: How to reset form to initial values?
814
- A: Call `form.reset()` for single field or `form.resetAll()` for GroupNode to reset all children.
815
-
816
- ### Q: How to get all form values?
817
- A: Access `form.value.value` (it's a Signal) or use `form.getValue()` method.
818
-
819
- ### Q: How to programmatically set multiple values?
820
- A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
821
-
822
- ### Q: Form instance recreated on every render?
823
- A: Wrap `createForm()` in `useMemo()`:
824
- ```typescript
825
- const form = useMemo(() => createForm<MyForm>({ form: schema }), []);
826
- ```
402
+ // forms/credit-application/schema.ts
403
+ import { loanInfoSchema } from './steps/loan-info/schema';
404
+ export const creditApplicationSchema = {
405
+ ...loanInfoSchema,
406
+ monthlyPayment: { value: 0, disabled: true },
407
+ };
827
408
 
828
- ### Q: How to handle form submission?
829
- A:
830
- ```typescript
831
- const handleSubmit = async (e: React.FormEvent) => {
832
- e.preventDefault();
833
- form.markAsTouched(); // Show all errors
834
- await form.validate(); // Run all validators
835
-
836
- if (form.valid.value) {
837
- const data = form.value.value;
838
- await submitToServer(data);
839
- }
409
+ // forms/credit-application/validators.ts
410
+ import { loanValidation } from './steps/loan-info/validators';
411
+ export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
412
+ loanValidation(path);
413
+ // Cross-step validation...
840
414
  };
841
415
  ```
842
416
 
843
- ## Links
417
+ ### Scaling
844
418
 
845
- - Repository: https://github.com/AlexandrBukhtatyy/ReFormer
846
- - Documentation: https://alexandrbukhtatyy.github.io/ReFormer/
847
- - Issues: https://github.com/AlexandrBukhtatyy/ReFormer/issues
419
+ | Complexity | Structure |
420
+ |------------|-----------|
421
+ | Simple | Single file: `ContactForm.tsx` |
422
+ | Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
423
+ | Complex | Full colocation with `steps/` and `sub-forms/` |