perf: stabilize useConditionalField default values with useRef and replace Zod superRefine with declarative unions for performance
This commit is contained in:
@@ -479,6 +479,77 @@ export function DepartmentForm() {
|
||||
|
||||
---
|
||||
|
||||
## Enterprise Performance Guidelines: Forms & Validation
|
||||
|
||||
When building large-scale ERP forms, seemingly trivial React or Zod patterns can catastrophically degrade performance at scale. Adhere strictly to the following optimizations.
|
||||
|
||||
### The "Unstable Default Value" Trap in Hooks
|
||||
|
||||
When creating custom form hooks (like `useConditionalField`), you often need to provide a fallback or default value. Passing an inline array or object as a `defaultValue` can trigger infinite render loops if it is included in a `useEffect` dependency array, because React's referential equality check fails on every render.
|
||||
|
||||
**Solution: The `useRef` Stabilization Pattern**
|
||||
|
||||
We resolve this by storing the `defaultValue` in a `useRef`. This allows the hook's cleanup logic to access the latest value without triggering the effect again:
|
||||
|
||||
```tsx
|
||||
// Inside useConditionalField.ts
|
||||
const defaultValueRef = useRef(defaultValue);
|
||||
|
||||
// Update ref on every render without triggering dependencies
|
||||
useEffect(() => {
|
||||
defaultValueRef.current = defaultValue;
|
||||
}, [defaultValue]);
|
||||
|
||||
// The main effect no longer depends on defaultValue
|
||||
useEffect(() => {
|
||||
if (!condition) {
|
||||
const targetValue = defaultValueRef.current !== undefined ? defaultValueRef.current : '';
|
||||
setValue(name, targetValue);
|
||||
}
|
||||
}, [condition, name, setValue]);
|
||||
```
|
||||
|
||||
### Zod Schema Performance: Avoid superRefine for Conditionals
|
||||
|
||||
For complex dynamic forms, developers often default to `.superRefine` or `.refine` to handle conditional validation (e.g., "Require Tax ID only if userType is Corporate").
|
||||
|
||||
**The Problem:** `superRefine` acts as an opaque callback. Zod cannot optimize it. In large forms, doing manual `.safeParse` inside a `superRefine` loop forces Zod to parse the entire tree continuously on every keystroke, leading to severe O(n) CPU spikes.
|
||||
|
||||
**The Solution:** Use declarative schema branching via `.and()`, `z.discriminatedUnion`, and `z.union`. These are statically analyzed by Zod and evaluated at native speed.
|
||||
|
||||
#### ❌ Bad: Manual Parsing (O(n) CPU Spike)
|
||||
|
||||
```tsx
|
||||
const badSchema = z.object({
|
||||
userType: z.enum(['PERSONAL', 'CORPORATE']),
|
||||
corporateTaxId: z.string().optional()
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.userType === 'CORPORATE') {
|
||||
// ⚠️ INCREDIBLY SLOW: Manual parsing inside refine loop
|
||||
const res = taxIdValidator.safeParse(data.corporateTaxId);
|
||||
if (!res.success) ctx.addIssue({ ...res.error.issues[0], path: ['corporateTaxId'] });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### ✅ Good: Declarative Unions (O(1) Evaluation)
|
||||
|
||||
```tsx
|
||||
const goodSchema = z.object({
|
||||
userType: z.enum(['PERSONAL', 'CORPORATE']),
|
||||
corporateTaxId: z.string().optional()
|
||||
}).and(
|
||||
z.discriminatedUnion('userType', [
|
||||
z.object({ userType: z.literal('PERSONAL') }),
|
||||
z.object({ userType: z.literal('CORPORATE'), corporateTaxId: taxIdValidator })
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
By stacking `.and(z.union([...]))` for independent conditionals (like `hasSpouse`, `newsletter`, etc.), you achieve lightning-fast, type-safe conditional validation without writing a single `superRefine` loop.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Form
|
||||
|
||||
Reference in New Issue
Block a user