Powrót do strony głównej

React Hook Form i Zod: doświadczenie rocznej eksploatacji

Roczne doświadczenie użytkowania React Hook Form i Zod w produkcji. Analiza zalet redukcji kodu szablonowego i identyfikacja ukrytych problemów, takich jak obsługa undefined, błędy typów i flaga isDirty. Rekomendacje dotyczące unikania typowych błędów.

React Hook Form i Zod: rzeczywiste przypadki z produkcji
Advertisement 728x90

# React Hook Form i Zod: rok w produkcji bez zbędnego kodu i ukrytych pułapek

Po roku intensywnego używania duetu React Hook Form i Zod w produkcji na dużym projekcie zespół potwierdza: deklaratywne podejście do formularzy redukuje objętość kodu o 30–40%, ale wymaga dbałości o detale implementacji. Przedstawiamy pułapki, na które natknęliśmy się, i sposoby ich obejścia.

Od MobX do deklaratywnych formularzy: dlaczego zmieniać podejście?

W poprzednich iteracjach projektu formularze budowano w stylu MVC na bazie MobX. Ta metoda pozwalała kontrolować stan, ale generowała zbędny kod: dla każdego pola potrzebne były oddzielne metody w kontrolerze, tablice błędów i ręczna synchronizacja z UI. Na przykład, dodanie nowego pola wymagało stworzenia:

export class SomeController {
  public setDuration = (v: number) => {
    this.store.duration = v;
  }
}

Taki szablonowy kod nie należał do logiki biznesowej, ale pochłaniał nawet 60% objętości komponentu. Przejście na React Hook Form (RHF) z Zod rozwiązało problem: schemat walidacji opisuje się deklaratywnie, a biblioteka automatyzuje synchronizację stanu i obsługę błędów.

Google AdInline article slot

Przykład podstawowego formularza:

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>
  );
}

Efekt: objętość kodu spadła, utrzymanie stało się prostsze, a tempo tworzenia nowych formularzy wzrosło. Jednak w trakcie eksploatacji wyszły na jaw niuanse, których nie opisano w dokumentacji.

Trzy pułapki w duecie React Hook Form i Zod

Przez rok używania RHF i Zod napotkaliśmy trzy kluczowe problemy, które mogą zakłócić działanie formularza w określonych scenariuszach.

Google AdInline article slot

1. Obsługa undefined i null

RHF nie obsługuje undefined jako wartości pola. Przy próbie ustawienia:

methods.setValue('age', undefined)

pole ignoruje zmianę. Dokumentacja o tym wspomina, ale nie jest to oczywiste. Standardowe rozwiązanie to użycie null i zadeklarowanie pola jako .nullable():

const schema = z.object({
  age: z.number().nullable()
});

methods.setValue('age', null);

Jednak to może kolidować z niestandardowymi komponentami oczekującymi liczb. Na przykład komponent wprowadzania wieku może się wysypać na null. Odwrotna sytuacja: komponent zwraca undefined przy czyszczeniu, ale RHF nie przetwarza tej wartości.

Google AdInline article slot

Dla przycisku resetowania formularza pojawia się dodatkowa komplikacja. Metoda reset() ustawia wartości z defaultValues, ale jeśli są zadeklarowane jako undefined, reset nie zadziała. Rozwiązanie — użyć null lub pustego ciągu dla pól tekstowych.

2. Nieoczekiwane błędy typów w Zod

Problem arises przy niezgodności typów danych. Na przykład schemat:

const schema = z.object({
  field1: z.string().min(1, 'Fill field').optional()
});

Jeśli komponent zwróci null (zamiast undefined), Zod zgłosi błąd invalid type (expected string received null). Użytkownik zobaczy komunikat systemowy zamiast twojego tekstu.

Rozwiązanie — jawnie podać tekst błędu dla wszystkich typów walidacji:

const schema = z.object({
  field1: z.string('Fill field').min(1, 'Fill field').optional()
});

To gwarantuje, że każdy błąd wyświetli twój komunikat.

3. Niestabilność flagi isDirty

Flaga formState.isDirty wskazuje, czy w formularzu zaszły zmiany. Jednak przy inicjalizacji z pustym defaultValues:

const methods = useForm({
  resolver: zodResolver(formSchema),
  defaultValues: {},
});

isDirty staje się true zaraz po wyrenderowaniu, choć użytkownik jeszcze nic nie zmienił. Przy tym dirtyFields pozostaje pustym obiektem.

To wynika z niezgodności struktury defaultValues i schematu. Aby uniknąć fałszywych alarmów, upewnij się, że defaultValues zawiera wszystkie pola zdefiniowane w schemacie, nawet z undefined lub null.

Jak unikać typowych błędów?

Na podstawie doświadczenia polecamy sprawdzone praktyki:

  • Do obsługi pustych wartości:

- Pola tekstowe: używaj pustego ciągu jako domyślnego.

- Pola liczbowe/obiektowe: stosuj .nullable() w Zod i null w wartościach.

- Zawsze sprawdzaj, czy niestandardowe komponenty obsługują wybrany typ.

  • Do niezawodnej walidacji:

- Podawaj ogólny tekst błędu dla wszystkich etapów walidacji, jak w powyższym przykładzie.

- Testuj scenariusze z null i undefined na etapie tworzenia komponentów.

  • Do poprawnego działania isDirty:

- Inicjalizuj defaultValues zgodnie ze schematem, w tym wszystkie pola.

- Unikaj pustego obiektu {} — jawnie wymieniaj pola z undefined lub null.

Dodatkowo zalecamy dodać do projektu wspólne helpery do pracy z formularzami, np. funkcję resetu uwzględniającą niuanse RHF:

const resetForm = () => {
  methods.reset({
    name: null,
    email: null,
    age: null,
  });
};

Co ważne

  • Redukcja kodu nie zwalnia z testowania: Deklaratywne podejście oszczędza czas, ale wymaga dokładnego sprawdzenia przypadków brzegowych.
  • Typizacja to twój sojusznik: Używanie Zod z TypeScript zapobiega wielu błędom, ale wymaga poprawnego skonfigurowania schematów.
  • Dokumentuj niuanse: Zapisz w wewnętrznej dokumentacji projektu obejścia dla typowych problemów z RHF i Zod.

— Editorial Team

Advertisement 728x90

Czytaj dalej