Tag: next.js

  • 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.

  • Headless WordPress z Next.js – kiedy ma sens, a kiedy to przerost formy

    Moje portfolio na luckadomena.pl działa w architekturze headless: WordPress pod adresem cms.luckadomena.pl służy wyłącznie do zarządzania treścią, a stronę, którą widzisz, generuje Next.js. Brzmi nowocześnie i takie jest, ale to nie jest rozwiązanie dla każdego projektu. W tym wpisie opisuję, kiedy headless WordPress ma sens, kiedy jest przerostem formy i jakie decyzje podjąłem przy własnej stronie.

    Czym jest headless WordPress?

    W klasycznym WordPressie jeden system robi wszystko: przechowuje treść, renderuje szablon PHP i wysyła gotowy HTML do przeglądarki. W podejściu headless „odcinamy głowę”, czyli warstwę prezentacji. WordPress zostaje panelem do pisania wpisów i udostępnia dane przez REST API (np. /wp-json/wp/v2/posts) albo GraphQL (wtyczka WPGraphQL). Frontend to osobna aplikacja, u mnie w Next.js, która te dane pobiera i sama decyduje, jak je wyświetlić.

    Kiedy headless WordPress ma sens

    • Chcesz pełnej kontroli nad frontendem. Żadnych ograniczeń motywu, żadnego page buildera – komponenty React, TypeScript i nowoczesne narzędzia.
    • Zależy Ci na wydajności. Strony można wygenerować z góry jako statyczny HTML, który ładuje się błyskawicznie i nie obciąża serwera przy każdym wejściu.
    • Bezpieczeństwo ma znaczenie. Panel WordPressa żyje na osobnej subdomenie, a publiczna strona to czyste pliki – nie ma tam PHP ani bazy danych, które można zaatakować.
    • Treść trafia w kilka miejsc. Te same dane z API może wykorzystać strona, aplikacja mobilna czy newsletter.
    • Redakcja zna WordPressa. Osoba pisząca treści dalej pracuje w znanym panelu, a deweloper ma swobodę po stronie frontendu.

    Kiedy to przerost formy

    • Klient chce sam układać strony w page builderze. Elementor czy Gutenberg z rozbudowanymi blokami tracą sens, bo frontend nie wie, jak je wyrenderować.
    • Projekt opiera się na wtyczkach działających na froncie. Formularze, koszyk WooCommerce, wtyczki SEO generujące meta tagi – w headless każdą z tych rzeczy trzeba zbudować lub podpiąć samodzielnie.
    • Mały budżet i brak dewelopera na stałe. Zamiast jednej aplikacji utrzymujesz dwie. Każda zmiana wyglądu to praca programisty, a nie kliknięcie w panelu.
    • Prosta wizytówka firmy. Dobrze zoptymalizowany klasyczny WordPress z lekkim motywem w zupełności wystarczy.

    Jak zrobiłem to w praktyce

    Najważniejsza decyzja dotyczyła sposobu renderowania. Next.js daje kilka możliwości:

    • SSG (Static Site Generation) – strony generowane raz, podczas budowania,
    • SSR (Server-Side Rendering) – strona generowana przy każdym wejściu, wymaga serwera Node.js,
    • ISR (Incremental Static Regeneration) – statyczne strony odświeżane w tle co określony czas, również wymaga serwera.

    Portfolio stoi na zwykłym hostingu współdzielonym, bez Node.js. Wybrałem więc pełny statyczny eksport: w konfiguracji ustawiam output: 'export', a podczas buildu Next.js pobiera z WordPressa wszystkie wpisy i dla każdego generuje osobny plik HTML. Listę adresów dostarcza funkcja generateStaticParams:

    export async function generateStaticParams() {
      const posts = await getPosts()
      return posts.map(post => ({ slug: post.slug }))
    }

    Dodatkowo włączyłem trailingSlash: true. Każda podstrona zapisuje się wtedy jako blog/nazwa-wpisu/index.html, a serwer Apache serwuje ją bez żadnych reguł przepisywania adresów.

    Cena tego wyboru jest jasna: nowy wpis na blogu pojawia się na stronie dopiero po ponownym zbudowaniu i wgraniu plików. Przy portfolio, gdzie treść zmienia się rzadko, to rozsądny kompromis w zamian za szybkość, bezpieczeństwo i tani hosting.

    Pułapki, na które trzeba się przygotować

    SEO jest po Twojej stronie

    Wtyczka SEO w WordPressie nie wygeneruje meta tagów na froncie w Next.js. Tytuły, opisy, Open Graph, mapę witryny i robots.txt trzeba zbudować samodzielnie – na szczęście App Router ma do tego wbudowane API (metadata, generateMetadata, sitemap.ts).

    Pobieraj tylko potrzebne dane

    REST API WordPressa domyślnie zwraca bardzo dużo pól. Parametr _fields pozwala ograniczyć odpowiedź do tego, czego naprawdę używasz, np. ?_fields=slug,title,excerpt,date. Mniej danych to szybszy build.

    Hydracja i dane z przeglądarki

    Strona generowana na etapie buildu nie ma dostępu do window ani localStorage. Jeśli zapisujesz w przeglądarce np. wybrany język albo ustawienia dostępności, odczyt musi być zrobiony tak, aby pierwszy render po stronie klienta zgadzał się z HTML-em z buildu. Ja użyłem do tego hooka useSyncExternalStore, który ma osobną wartość dla serwera i dla przeglądarki.

    Obrazy

    Wbudowana optymalizacja obrazów w Next.js działa na serwerze, więc przy statycznym eksporcie trzeba ją wyłączyć (images.unoptimized) i zadbać o rozmiary oraz formaty plików już w WordPressie.

    Podsumowanie: krótka checklista decyzji

    • Czy potrzebujesz pełnej kontroli nad frontendem i wydajności na najwyższym poziomie? Headless ma sens.
    • Czy klient chce sam budować strony w page builderze i korzystać z wielu wtyczek frontowych? Zostań przy klasycznym WordPressie.
    • Czy masz hosting z Node.js? Jeśli nie, statyczny eksport to dobre wyjście – pod warunkiem, że akceptujesz ponowny build przy każdej zmianie treści.

    Headless WordPress to narzędzie, nie cel sam w sobie. W moim przypadku pozwolił połączyć wygodny panel do pisania z szybkim, nowoczesnym frontendem – i właśnie dlatego go wybrałem.