Dlaczego frontend jest trudny po React: stan, efekty, granice i narzędzia AI
React ułatwił tworzenie interfejsów użytkownika, ale nie ich projektowanie. Tam, gdzie znajduje się stan, pojawiają się skomplikowane efekty, granice systemu oraz kod generowany przez AI – to właśnie stanowi prawdziwą pracę przy frontendzie.
Źródła: React Isn’t the Hardest Part of Frontend Development
Część programistów front-end potrafi stworzyć komponent React w dwadzieścia minut, a następnie spędzić kolejne trzy godziny na próbach zrozumienia, dlaczego dalsza obsługa tego komponentu jest niemożliwa. Winowajcą nie jest JSX, nie jest to useState, ani też nie jest to właściwie React. To, co sprawia, że praca nad front-endem jest trudna, to elementy otaczające framework: znajomość tego, jak produkt powinien się zachowywać, wybór miejsca przechowywania stanu, koordynacja wywołań asynchronicznych, określanie granic komponentów, radzenie sobie z osobliwościami przeglądarki, zapewnienie dostępności i szybkości interfejsów oraz tworzenie kodu, który nadal ma sens po pół roku.
React ułatwił tworzenie interfejsów. Nigdy jednak nie ułatwił pracy nad front-endem, a ta dziedzina zbyt długo udaje, że tak jest.
Co naprawił React, a co pozostawił bez zmian
Zanim React przejął stery, trudności miały inny charakter. Programiści edytowali DOM ręcznie i łączyli obsługę zdarzeń pojedynczo, mając trudności z utrzymaniem ekranu zgodnego z danymi aplikacji. W miarę rozwoju aplikacji stawało się coraz trudniej prześledzić, w jaki sposób działania użytkownika, dane oraz wyświetlane elementy są ze sobą powiązane.
React zaproponował przydatny model myślowy: interfejs użytkownika jest funkcją stanu. Zamiast instruować przeglądarkę przy każdej zmianie w DOM, opisuje się, jak wygląda interfejs dla danego stanu. Komponenty określają granice, a atrybuty dostarczają wyraźne dane wejściowe; stan reprezentuje dane, które zmieniają się z upływem czasu, a deklaratywne renderowanie eliminuje wiele niepotrzebnych złożoności. To był prawdziwy krok naprzód.
Mimo to framework rozwiązuje tylko te problemy, do których został stworzony. React nie ma żadnej opinii na temat tego, czy aplikacja potrzebuje pięciu elementów stanu globalnego, czy pięćdziesięciu. Nie powie, gdy komponent wykonuje zbyt wiele zadań, czy projekt API jest rozsądny, czy elementy wizualne znajdują się we właściwych miejscach, ani czy proces zakupowy ma sens dla klienta. Z pewnością też nie może zdecydować, co powinien zrobić produkt, gdy połączenie zostanie przerwane w trakcie żądania. Wszystko to to również problemy frontendu.
Zwykła funkcja w starciu z rzeczywistością
Weźmy zwykłą panel kontrolny. Użytkownik go otwiera, aplikacja pobiera dane, zmienia filtr, wyniki się aktualizują, użytkownik wybiera pozycję, otwiera się okno modalne, edytuje i zapisuje zmiany, serwer odpowiada, a panel kontrolny się aktualizuje. Nic z tego nie brzmi trudno.
Teraz dodajmy to, co faktycznie się dzieje. Pierwsza prośba zawodzi. Filtr zmienia się, gdy wcześniejsza prośba jest jeszcze w trakcie przetwarzania, a starsza odpowiedź przychodzi po nowszej. Okno modalne otwiera się za pomocą głębokiego linku. Strona jest odświeżana w trakcie edycji. Otwarte są dwa karty. Sesja wygasa. API zwraca częściowe dane. Serwer akceptuje zmianę, ale klient nigdy nie widzi odpowiedzi, a użytkownik kliknie „Zapisz” dwa razy. Lista zawiera 10 000 wierszy, połączenie jest wolne i odbywa się przez urządzenie mobilne, interfejs musi funkcjonować przy użyciu klawiatury, a komunikat o błędzie musi poinformować użytkowników, co mogą zrobić dalej.
W tym momencie kod React jest najmniej interesującą częścią. Wszystko to można zbudować za pomocą React, ale można to zrobić również źle. Wielu programistów nie dostrzega tej różnicy.
Zadaniem jest zarządzanie złożonością
Praca programisty frontend nie polega głównie na pisaniu komponentów. Chodzi o opanowanie złożoności, aby użytkownik widział coś prostego, co jest o wiele trudniejsze. Dobrze napisany komponent może składać się z pięćdziesięciu linijek kodu, ale dotarcie do tej liczby zamiast pięciuset wymaga serii decyzji:
- Skąd pochodzą dane, kto nimi zarządza, jak długo istnieją i kto może je zmieniać
- Co się dzieje, gdy żądanie zawodzi
- Czy stan powinien przetrwać zmianę strony lub odświeżenie
- Czy powinien znajdować się w adreście URL, w lokalnym stanie komponentu czy w pamięci serwera
- Czy dwa komponenty powinny dzielić się samymi danymi, czy też obliczać podobne wartości samodzielnie
React nie odpowiada na żadne z tych pytań. Programista to robi, a odpowiedzi na nie się kumulują. Słaba decyzja rzadko pozostaje w obrębie jednego komponentu – przekształca się w wzorzec, który kopiują inni programiści, a wkrótce aplikacja ma spójną architekturę, której nikt nie wybrał. W ten sposób bazy kodu frontendu stają się problematyczne: decyzje się gromadzą, a złożoność Reacta ma z tym niewiele wspólnego.
Stan jako problem modelowania
Szybkim sposobem na wykrycie niedojrzałego projektu frontendu jest przyjrzenie się jego stanowi. Intensywne używanie useState samo w sobie nie jest błędem. Problem polega na traktowaniu stanu jako miejsca na przechowywanie wartości, a nie jako czegoś, co należy zmodelować. Programista widzi zmieniającą się wartość i sięga po odpowiedni hook.
const [value, setValue] = useState(...)
Wygląda na to, że jest gotowe, ale pytanie nie brzmi, jak przechować tę wartość. Pytanie brzmi, czy w ogóle powinna ona istnieć. Spójrzcie na tę trójkę:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
Obecnie trzy elementy systemu przechowują informacje, które można by uzyskać z dwóch. To nie React jest winny – winny jest model myślowy.
To samo dzieje się na większą skalę. Zespoły przechowują odpowiedzi API, przefiltrowane kopie tych odpowiedzi, flagi ładowania, obliczone liczby, wybrane rekordy, flagi modali oraz duplikaty jednej entity w kilku miejscach. Z czasem nikt nie potrafi określić, która kopia jest autorytatywna, co prowadzi do błędów. Jeden komponent zmienia kopię, podczas gdy inny czyta inną. Efekt synchronizacji próbuje je zsynchronizować, a inny efekt reaguje na pierwszy. Nagle przeglądarka zawiera mały system rozproszony. Tę architekturę nie stworzył React – zrobili to ludzie piszący kod.
Efekty ujawniają projekt
Jeśli jakaś funkcja React ponosi większą winę niż pozostałe, to jest to useEffect, a krytyka wobec niej jest często uzasadniona. Jednak to właśnie w efektach tego typu najczęściej pojawiają się problemy architektoniczne, a nie tam, gdzie one się zaczynają.
Wyobraź sobie komponent zawierający kilka takich efektów: jeden pobiera dane, drugi synchronizuje stan, trzeci reaguje na zmianę właściwości, czwarty zapisuje dane do lokalnego przechowania, piąty wysyła dane do systemu analitycznego, a szósty resetuje coś, gdy zmienia się określona warunek. Każdy z nich wydaje się sensowny osobno. Razem tworzą jednak graf zależności, który jest trudny do zrozumienia. Programista dodaje kolejne zależności, dopóki narzędzie do sprawdzania kodu nie przestanie wykrywać błędów, albo usuwa niektóre, ponieważ efekt jest uruchamiany zbyt często, albo dodaje referencję typu boolean, aby uniknąć podwójnego uruchomienia. W końcu komponent działa w testowanym scenariuszu, choć nikt dokładnie nie wie dlaczego.
To nie jest wada Reacta. Chodzi o niejasną przypisanie odpowiedzialności oraz skomplikowane efekty uboczne, a React po prostu dostarcza im miejsce do istnienia.
Przeglądarka to tylko jeden węzeł w większym systemie
Zadania związane z frontendem stale się rozszerzają. Współczesne aplikacje komunikują się z API i bazami danych, systemami autoryzacji oraz narzędziami analitycznymi, a także z flagami funkcjonalnymi, dostawcami płatności i innymi usługami third-party. Do tego dochodzą funkcje typu edge, komponenty serwerowe, zadania w tle oraz, w coraz większym stopniu, usługi oparte na sztucznej inteligencji. Przeglądarka jest zaledwie jednym uczestnikiem o wiele większego systemu, dlatego programista frontendu musi znać nie tylko aspekty renderowania, ale także to, co dzieje się przed i po wysłaniu żądania z strony.
To nie oznacza, że wszyscy muszą zostać specjalistami od backendu. Oznacza to, że stara podziałka na frontend i backend staje się coraz mniej przydatna. Osoba, która nie zna protokołu HTTP, w końcu będzie miała problemy z zarządzaniem pamięcią cache. Ktoś, kto nie zna bazy danych, będzie miał trudności z rozumieniem koncepcji spójności danych, a osoba, która nie rozumie mechanizmów autoryzacji, napotka problemy z zachowaniem sesji. Warunki konkurencji i próby ponownych działań dezorientują programistów bez wiedzy z zakresu systemów rozproszonych, a ci, którzy nie rozumieją potrzeb użytkowników, tworzą eleganckie interfejsy, którymi nikt nie chce korzystać. Framework stanowi pojedynczą warstwę.
Sztuczna inteligencja zmienia aspekty ekonomiczne, a nie trudne elementy techniczne
Asystenci do programowania oparte na AI zmieniły koszty pracy nad interfejsem użytkownika. Tworzenie komponentów, form, konwersji projektu na JSX, hooków oraz testów staje się coraz tańsze. Może to brzmieć jak zagrożenie, lecz lepiej postrzegać to jako sprawdzenie tego, gdzie rzeczywiście leży wartość. Jeśli twoim głównym wkładem jest szybkie pisanie kodu JSX, AI stanowi problem. Jeśli natomiast chodzi o zrozumienie systemów, architektury, zachowania produktu, kompromisów oraz potrzeb użytkowników, AI modyfikuje twój sposób pracy, pozostawiając trudne zadania bez zmian i prawdopodobnie zwiększając ich znaczenie.
Asystent może w ciągu sekund przygotować trzy prawdopodobne wersje funkcji. Wybór spośród nich to decyzja architektoniczna, a nie składniowa. Może stworzyć rozwiązanie do zarządzania stanem, ale czy ten stan powinien być globalny? Może napisać kod do optymistycznego aktualizowania, ale czy aplikacja rzeczywiście powinna zachowywać się w sposób optymistyczny? Może dodać pamięć cache, ale jaka jest strategia unieważniania danych? Może napisać efekt, ale czy ten efekt w ogóle powinien istnieć? Im szybciej pojawia się kod, tym bardziej potrzebna jest rozwaga, a to najprawdopodobniej będzie definiować pracę nad interfejsem użytkownika w najbliższych latach.
Zdolni deweloperzy frontendu zwykle myślą mniej o React i więcej o granicach funkcjonowania aplikacji. Zadają pytania o to, do kogo należy odpowiedzialność, i zauważają, gdy komponent przekształca się w małą aplikację. Kwestionują duplikację stanu, planują scenariusze awarii jeszcze przed realizacją optymalnego przebiegu działania, wiedzą, co faktycznie gwarantuje API, i zwracają uwagę na nieoczekiwane zachowania użytkowników. Potrafią ocenić, kiedy abstrakcja jest przydatna, a kiedy jedynie ukrywa złożoność. Rozumieją działanie przeglądarki, optymalizują wydajność, nie przyspieszając nadmiernie wszystkiego, a przede wszystkim potrafią wyjaśnić, dlaczego system funkcjonuje w określony sposób. To umiejętność znacznie trudniejsza do zautomatyzowania i trwa dłużej niż znajomość najnowszego API React.
Wiedza o frameworku wygasa
Rozwijający nie omawiają tego wystarczająco często: wiedza o frameworkach ma określony czas przydatności. API React poznanego pięć lat temu nie jest już tak aktualne jak dzisiaj. Najlepsze praktyki się zmieniają, biblioteki znikają, narzędzia do budowania aplikacji ewoluują, strategie renderowania się rozwijają, możliwości serwerów rosną, a pojawiają się nowe frameworki. Składnia się zmienia, ale podstawowe problemy inżynieryjne pozostają niezwykle stabilne:
- Modelowanie stanu i przesyłanie danych w systemie
- Radzenie sobie z awariami i zarządzanie pracą asynchroniczną
- Unikanie niepotrzebnego powiązania komponentów
- Czynienie systemu obserwowalnym, a interfejs dostępnym
- Kontrolowanie złożoności
Problemy te przetrwały jQuery, Angular i Backbone, a także wczesny ekosystem React. Przetrwają również to, co nadejdzie później. Dlatego nauka frontendu wyłącznie poprzez frameworki to kruchy plan. Naukij framework, ale nie myl go z całą dyscypliną niezbędną do pracy z nim.
React może kiedyś przestać być centrum rozmów. Może go zastąpić coś innego, mogą współistnieć kilka podejść, albo może pozostać dominujący znacznie dłużej, niż się spodziewano. To nie ma większego znaczenia. Programiści, których tożsamość opiera się na API React, będą musieli je odbudować, podczas gdy ci, którzy nauczyli się, jak naprawdę działają interfejsy, będą się dostosowywać.
React to narzędzie, a nie sztuka
Rozwój frontendu nigdy nie polegał w swej istocie na React. React to bardzo dobre narzędzie do tworzenia aplikacji frontendowych, i nadal nim pozostaje. Trudne jest podjęcie decyzji o tym, co należy przedstawić: jak elementy ze sobą współpracują, co się dzieje, gdy coś się psuje, gdzie znajduje się stan, co działa w przeglądarce a co na serwerze, jak system rozwija się wraz z produktem oraz jak ukryć to wszystko przed osobą, która go używa. To jest właśnie zadanie, a JSX to po prostu sposób zapisywania części tego procesu.
W miarę jak narzędzia stają się lepsze w tworzeniu kodu, jego tworzenie traci na wartości. To nie kończy rozwoju oprogramowania, choć zmienia to, nad czym muszą się zastanawiać programiści. Osoba, która potrafi szybko stworzyć komponent, jest przydatna, ale jeszcze bardziej cenna jest ta, która potrafi ocenić, czy taki komponent w ogóle powinien istnieć. To samo dotyczy integracji API: stworzenie takiej integracji jest przydatne, ale wykrycie błędu w samym kontrakcie ma większą wartość. Programista, który potrafi poprosić sztuczną inteligencję o refaktoryzację bazy kodu, ma wartość, a ten, który potrafi ocenić, czy proponowana refaktoryzacja poprawia system, ma jeszcze większą.
Dlatego nikt nie powinien się martwić tym, że React staje się łatwiejszy w użyciu. To właśnie jest celem – funkcją frameworka jest eliminacja niepotrzebnej pracy, a doświadczony programista wykorzystuje zaoszczędzony czas na rozwiązywanie istotnych problemów.
Literatura pokrewna
- Budowanie agenta ReAct w LangChain: Executorzy, tablice robocze i ich ograniczenia — Stwórz agenta ReAct wykorzystującego narzędzia, zobacz, jak agent i AgentExecutor dzielą pracę, i dowiedz się, dlaczego LangGraph lepiej radzi sobie z przepływem sterowania, pętlami i stanem.
- Gdy memoizacja w React działa przeciwnie: dlaczego mniej renderowań spowolniło aplikację — Jak memo, useMemo i useCallback wprowadziły dodatkowe koszty i błędy na dużej stronie React, oraz dlaczego przeniesienie stanu i analiza całych interakcji to naprawiło.