Nie potrzebujesz useEffect – 3 poprawki, które zrobiłem w kodzie swojego portfolio

useeffect czy aby na pewno potrzebny

Przy porządkowaniu kodu mojego portfolio przed publikacją linter zgłosił ten sam błąd w trzech różnych komponentach: „Calling setState synchronously within an effect can trigger cascading renders”. Kod działał, strona wyglądała dobrze – a mimo to wszystkie trzy przypadki były do poprawy. W tym wpisie pokazuję, co było nie tak, jak to naprawiłem i czego nauczyło mnie to o useEffect.

Do czego naprawdę służy useEffect?

useEffect służy do synchronizacji komponentu z czymś spoza Reacta: timerem, subskrypcją zdarzeń, ręczną zmianą DOM, zewnętrzną biblioteką. Nie służy do wyliczania stanu z innego stanu ani do „ustawienia czegoś zaraz po pierwszym renderze”. Gdy w efekcie od razu wywołujemy setState, React renderuje komponent, uruchamia efekt, a potem musi wyrenderować go jeszcze raz. To niepotrzebna praca, a czasem widoczne mignięcie złej wartości.

Przypadek 1: flaga isMounted

Popularny wzorzec wyglądał tak:

const [isMounted, setIsMounted] = useState(false)

useEffect(() => {
  setIsMounted(true)
}, [])

Chodzi o to, żeby wiedzieć, czy kod działa już w przeglądarce, i uniknąć błędu hydracji – sytuacji, w której HTML wygenerowany na serwerze różni się od pierwszego renderu po stronie klienta. React ma do tego lepsze narzędzie: useSyncExternalStore, który przyjmuje osobną wartość dla serwera i dla przeglądarki.

const subscribe = () => () => {}

export function useIsClient() {
  return useSyncExternalStore(subscribe, () => true, () => false)
}

Przypadek 2: odczyt z localStorage

Wybrany język strony i ustawienia dostępności zapisywałem w localStorage, a przy starcie kopiowałem je do stanu komponentu w efekcie. Problem polegał na tym, że ta sama informacja istniała w dwóch miejscach: w przeglądarce i w stanie Reacta. Rozwiązaniem było potraktowanie localStorage jako zewnętrznego magazynu danych i odczytywanie go przez useSyncExternalStore:

const storedLocale = useLocalStorageItem('site-locale')
const locale = normalizeLocale(storedLocale)

Język jest teraz wyliczany, a nie kopiowany. Jest jedno źródło prawdy, wszystkie komponenty czytające ten klucz są zsynchronizowane – także między kartami przeglądarki – a flaga isMounted stała się zbędna.

Przypadek 3: animacja pisania tekstu

Komponent wyświetlający kolejne słowa litera po literze miał trzy osobne stany: indeks słowa, liczbę widocznych znaków i informację, czy tekst jest właśnie kasowany. Większość zmian odbywała się w timerze, ale przejście do następnego słowa działo się bezpośrednio w efekcie. Głębsza przyczyna była taka, że trzy stany opisywały jedną rzecz – fazę animacji.

Przepisałem go na mały automat stanów z useReducer. Faza jest jedna: pisanie, pauza albo kasowanie, a cała logika przejść siedzi w czystej funkcji:

function advanceRotator(state, { activeWordLength, wordCount }) {
  switch (state.phase) {
    case 'typing':
      // add one character, switch to 'holding' when the word is complete
    case 'holding':
      return { ...state, phase: 'deleting' }
    case 'deleting':
      // remove one character, move to the next word at the end
  }
}

Efekt robi teraz tylko jedno: ustawia timer, który wywołuje dispatch, i sprząta go w funkcji czyszczącej. Nie da się też uzyskać nielogicznej kombinacji stanów, która wcześniej była teoretycznie możliwa.

Czego się nauczyłem

  • Zanim napiszesz useEffect, zapytaj: z czym się synchronizuję? Jeśli odpowiedź brzmi „z niczym spoza Reacta”, prawdopodobnie efekt nie jest potrzebny.
  • Nie kopiuj danych do stanu, jeśli możesz je wyliczyć. Jedno źródło prawdy eliminuje całą klasę błędów z rozjechanymi wartościami.
  • Kilka powiązanych stanów to sygnał do useReducer. Jawne fazy i czysta funkcja przejść są czytelniejsze i łatwiejsze do przetestowania.
  • Dane z zewnętrznych źródeł czytaj przez useSyncExternalStore. Rozwiązuje problem hydracji bez obejść w postaci flag.
  • Linter to sojusznik, nie przeszkoda. Reguły z React 19 wyłapały problemy, których nie widać w działaniu strony, a które z czasem prowadzą do trudnych do znalezienia błędów.

Podsumowanie

Wszystkie trzy komponenty działały przed poprawkami – i to jest najciekawsza lekcja. Kod, który „działa”, nie zawsze jest kodem dobrym. Po zmianach komponenty są prostsze, mają mniej stanu, renderują się rzadziej, a ich logika jest jawna. Jeśli Twój linter zgłasza set-state-in-effect, potraktuj to jako zaproszenie do uproszczenia kodu, a nie przeszkodę do wyciszenia.

Komentarze

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *