Tag: usereducer

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

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

    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.