React Hook Form a Zod: Rok v produkci bez zbytečného kódu a skrytých úskalí
Po roce aktivního používání kombinace React Hook Form a Zod v produkci velkého projektu tým potvrzuje: deklarativní přístup k formulářům snižuje objem kódu o 30–40 %, ale vyžaduje pozornost k detailům implementace. Povídejme, jaké úskalí jsme objevili a jak je překonat.
Od MobX k deklarativním formulářům: proč měnit přístup?
V předchozích iteracích projektu se formuláře stavěly na MobX v MVC stylu. Tato metoda umožňovala kontrolu stavu, ale generovala zbytečný kód: pro každé pole byly potřeba samostatné metody v kontroleru, pole chyb a ruční synchronizace s UI. Například přidání nového pole znamenalo vytvoření:
export class SomeController {
public setDuration = (v: number) => {
this.store.duration = v;
}
}
Takový šablonový kód nesouvisel s obchodní logikou, ale zabíral až 60 % objemu komponenty. Přechod na React Hook Form (RHF) se Zod tento problém vyřešil: schéma validace se popisuje deklarativně a knihovna automatizuje synchronizaci stavu a zpracování chyb.
Příklad základního formuláře:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
name: z.string().min(1, 'Enter name'),
email: z.string().email('Invalid email'),
age: z.coerce.number().min(0, 'Invalid vozrast').optional(),
});
type FormValues = z.infer<typeof formSchema>;
export function ExampleForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
},
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<div>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<input type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input type="number" {...register('age')} />
{errors.age && <span>{errors.age.message}</span>}
</div>
<button type="submit">Send</button>
</form>
);
}
Výsledek: objem kódu se snížil, údržba se zjednodušila a rychlost vývoje nových formulářů vzrostla. Během provozu se však projevily nuanse, které dokumentace nepopisuje.
Tři úskalí v kombinaci React Hook Form a Zod
Během roku používání RHF a Zod jsme narazili na tři klíčové problémy, které mohou narušit fungování formuláře v určitých scénářích.
1. Zpracování undefined a null
RHF nepodporuje undefined jako hodnotu pole. Při pokusu nastavit:
methods.setValue('age', undefined)
pole změnu ignoruje. V dokumentaci je to uvedeno, ale není to zřejmé. Standardní řešení je použít null a deklarovat pole jako .nullable():
const schema = z.object({
age: z.number().nullable()
});
methods.setValue('age', null);
To však může kolidovat s vlastními komponentami, které očekávají číslo. Například komponenta pro zadávání věku se může pokazit při obdržení null. Opakovaná situace: komponenta vrací undefined při čištění, ale RHF tuto hodnotu nezpracuje.
Pro tlačítko resetu formuláře vzniká další složitost. Metoda reset() nastavuje hodnoty z defaultValues, ale pokud jsou deklarovány jako undefined, reset nefunguje. Řešení je použít null nebo prázdný řetězec pro textová pole.
2. Neočekávané chyby typů v Zod
Problém vzniká při nesouladu typů dat. Například schéma:
const schema = z.object({
field1: z.string().min(1, 'Fill field').optional()
});
Pokud komponenta vrátí null (nikoli undefined), Zod vyhodí chybu invalid type (expected string received null). Uživatel uvidí systémovou zprávu místo vaší.
Řešení je explicitně uvést text chyby pro všechny typy validace:
const schema = z.object({
field1: z.string('Fill field').min(1, 'Fill field').optional()
});
To zaručuje, že jakákoli chyba zobrazí vaši zprávu.
3. Nestabilita příznaku isDirty
Příznak formState.isDirty ukazuje, zda došlo ke změnám ve formuláři. Při inicializaci s prázdným defaultValues:
const methods = useForm({
resolver: zodResolver(formSchema),
defaultValues: {},
});
se isDirty stane true hned po renderu, i když uživatel ještě nic nezměnil. Přitom dirtyFields zůstává prázdným objektem.
Děje se to kvůli nesouladu struktury defaultValues a schématu. Aby se zabránilo falešným spuštěním, ujistěte se, že defaultValues obsahuje všechna pole deklarovaná v schématu, i s undefined nebo null.
Jak se vyhnout typickým chybám?
Na základě zkušeností nabízíme ověřené postupy:
- Pro zpracování prázdných hodnot:
- Textová pole: použijte prázdný řetězec jako výchozí.
- Číselná/objektová pole: použijte .nullable() v Zod a null v hodnotách.
- Vždy ověřte, že vlastní komponenty podporují vybraný typ.
- Pro spolehlivou validaci:
- Uveďte obecný text chyby pro všechny fáze validace, jako v příkladu výše.
- Testujte scénáře s null a undefined již ve fázi vývoje komponent.
- Pro správné fungování isDirty:
- Inicializujte defaultValues v souladu se schématem, včetně všech polí.
- Vyhněte se prázdnému objektu {} – explicitně vyjmenujte pole s undefined nebo null.
Dále doporučujeme přidat do projektu jednotné pomocníky pro práci s formuláři, například funkci pro reset, která zohledňuje nuanse RHF:
const resetForm = () => {
methods.reset({
name: null,
email: null,
age: null,
});
};
Co je důležité
- Snížení kódu nezrušuje potřebu testování: Deklarativní přístup šetří čas, ale vyžaduje důkladnou kontrolu okrajových případů.
- Typizace je váš spojenec: Použití Zod s TypeScript předchází mnoha chybám, ale vyžaduje správné nastavení schémat.
- Dokumentujte nuanse: Zaznamenejte ve vnitřní dokumentaci projektu obchody pro typické problémy RHF a Zod.
— Editorial Team
Zatím žádné komentáře.