ReactJS + Redux: szybki kurs
React przekształcił front wokół komponentów i jednokierunkowego przepływu danych; Redux dodał przewidywalne zarządzanie stanem. Deep-dive o obu dla programisty Rails — komponenty, propsy i stan, pętla Redux — i kiedy Redux to przesada.
React stał się domyślnym sposobem budowania naprawdę aplikacjopodobnych frontów, a dla programisty Rails przybywającego z renderowanych po stronie serwera widoków (i może wycieczki przez AngularJS) warto zrozumieć go porządnie — zarówno model komponentowy, który czyni React zrozumiałym, jak i wzorzec Redux, który tak często jedzie obok. Oto szybki kurs obu, z uczciwym spojrzeniem na to, kiedy Redux zarabia na swoje miejsce, a kiedy jest ceremonią.
React: UI jako funkcja stanu
Rdzenną ideą Reacta jest prosta i potężna: twoje UI jest funkcją stanu. Opisujesz, jak interfejs powinien wyglądać dla danego zbioru danych, a gdy dane się zmieniają, React ustala minimalne aktualizacje DOM, by dopasować. Nigdy imperatywnie nie szturchasz DOM („ustaw ten tekst, przełącz tę klasę”) tak, jak z jQuery; zmieniasz stan i deklarujesz wynik.
Jednostką konstrukcji jest komponent — samodzielny kawałek UI, który bierze wejścia (propsy) i zwraca markup (JSX). Komponenty komponują się w drzewa, małe budują duże:
function ProductCard({ product, onAdd }) {
return (
<div className="card">
<h3>{product.name}</h3>
<span className="price">{formatPrice(product.price)}</span>
<button onClick={() => onAdd(product.id)}>Dodaj do koszyka</button>
</div>
)
}
function Catalog({ products, onAdd }) {
return products.map(p => <ProductCard key={p.id} product={p} onAdd={onAdd} />)
}
Dwie zasady definiują, jak dane się poruszają. Propsy płyną w dół: rodzic przekazuje dane
dzieciom, a dzieci nie mogą zmienić swoich propsów — to wejścia tylko-do-odczytu. Zdarzenia płyną
w górę: gdy coś dzieje się w dziecku, woła funkcję, którą dał mu rodzic (onAdd), więc rodzic
posiada zmianę. Ten jednokierunkowy przepływ danych to serce Reacta i rzecz, która czyni duże UI
opanowalnymi: dane mają jeden kierunek, więc gdy coś jest nie tak, śledzisz to po jednej ścieżce, a
nie przez plątaninę dwukierunkowych wiązań, które czyniły aplikacje AngularJS trudnymi do
ogarnięcia przy skali.
Stan i gdzie żyje
Propsy to wejścia z góry; stan to dane, które komponent sam posiada. Od React 16.8 (właśnie
wydanego) nowoczesnym sposobem trzymania stanu w komponencie funkcyjnym jest hook useState:
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
Krytyczną dyscypliną jest to, gdzie żyje stan. Stan powinien żyć w najniższym wspólnym przodku komponentów, które go potrzebują, a „podnosisz go w górę” tylko tak daleko, jak konieczne. Stan licznika należy do licznika; koszyk współdzielony przez wiele komponentów należy wyżej. Złap to umieszczenie dobrze, a większość aplikacji nie potrzebuje nic więcej niż stanu komponentu plus propsów — co jest wstępem do pytania o Redux.
Redux: pojedynczy, przewidywalny store
Gdy aplikacja rośnie, część stanu jest potrzebna wszędzie w drzewie — zalogowany użytkownik, koszyk, ustawienia całej aplikacji — a przewlekanie go przez propsy przez wiele warstw („prop drilling”) staje się bolesne. Redux to ustalona odpowiedź: wyciągnij ten współdzielony stan z drzewa komponentów do pojedynczego store’a, ze ścisłym, przewidywalnym wzorcem jego zmieniania.
Pętla Redux ma trzy części. Akcje to zwykłe obiekty opisujące, co się stało. Reducery to czyste funkcje biorące bieżący stan i akcję i zwracające nowy stan. Store trzyma stan i uruchamia reducery. Przepływ jest sztywno jednokierunkowy:
// akcja — opisuje, co się stało
{ type: "CART_ADD", productId: 42 }
// reducer — czysty: (state, action) => nowy stan, bez mutacji
function cart(state = [], action) {
switch (action.type) {
case "CART_ADD": return [...state, action.productId]
case "CART_REMOVE": return state.filter(id => id !== action.productId)
default: return state
}
}
// komponent dispatchuje akcję; store się aktualizuje; zasubskrybowane komponenty renderują się ponownie
dispatch({ type: "CART_ADD", productId: 42 })
Dyscyplina jest tym, co daje Reduxowi wartość: stan jest zmieniany tylko przez dispatchowanie akcji przez czysty reducer, który nigdy nie mutuje, lecz zwraca nowy stan. Ponieważ każda zmiana jest jawną, zalogowaną akcją, a każdy reducer jest deterministyczny, stan całej aplikacji staje się przewidywalny i śledzalny — widzisz dokładnie, co się zmieniło, kiedy i dlaczego (Redux DevTools pozwalają nawet odtwarzać akcje i podróżować w czasie). Dla złożonej aplikacji z dużą ilością współdzielonego, oddziałującego stanu ta przewidywalność jest naprawdę cenna.
Kiedy Redux to przesada
Oto uczciwa część, i to ten sam osąd, który przewija się przez wszystkie te wpisy: większość aplikacji sięga po Redux za wcześnie. Redux dodaje realny boilerplate — akcje, reducery, store, podpinanie komponentów do niego — a ta ceremonia opłaca się tylko, gdy naprawdę masz złożony, szeroko współdzielony stan. Dla bardzo wielu aplikacji zwykły stan komponentu plus przekazywanie propsów (i, dla problemu prop drilling, wbudowany Context Reacta dla kilku globalnych wartości) jest w pełni wystarczający i znacznie prostszy.
Sygnały, że faktycznie potrzebujesz Reduxa: stan, który wiele odległych komponentów czyta i zapisuje, złożone interakcje stanu trudne do śledzenia albo realna potrzeba śledzenia i debugowania zmian stanu w czasie. Sygnały, że nie: garść formularzy i trochę lokalnego stanu UI, co stan komponentu obsługuje dobrze. Przyjmij Redux, gdy przewidywalność, którą kupuje, jest warta boilerplate’u, który kosztuje — i nie wcześniej, bo Redux-wszystko to klasyczny sposób niepotrzebnego skomplikowania prostej aplikacji.
Dla programisty Rails
Zmiana myślowa wobec renderowanego po stronie serwera Rails to ta wielka: w Rails serwer posiada stan i renderuje HTML; w React klient posiada stan UI i renderuje się z niego, rozmawiając z twoją aplikacją Rails jako API JSON. Sięgaj po React, gdy front jest naprawdę aplikacjopodobny (bogaty, ze stanem, interaktywny) — i pamiętaj punkt wcześniejszego wpisu, że dla aplikacji treści-i-formularzy Stimulus nad renderowanymi po stronie serwera widokami jest często lepszym, lżejszym dopasowaniem. React to właściwe narzędzie dla prawdziwych aplikacji klienta, nie do posypywania interaktywności na w większości renderowaną po stronie serwera witrynę.
Werdykt
Model Reacta — UI jako funkcja stanu, zbudowane z komponowalnych komponentów, z propsami płynącymi w dół i zdarzeniami płynącymi w górę — to naprawdę lepszy sposób budowania złożonych frontów niż imperatywne jQuery czy dwukierunkowo wiązany AngularJS, które były wcześniej, bo jednokierunkowy przepływ danych trzyma duże UI śledzalnymi. Redux rozszerza tę przewidywalność na stan ogólnoaplikacyjny przez swoją ścisłą pętlę akcja/reducer/store — ale to boilerplate, który powinieneś dodać tylko, gdy złożony współdzielony stan naprawdę tego potrzebuje, nie odruchowo. Naucz się modelu komponentowego najpierw i opieraj na lokalnym stanie i propsach; wprowadź Redux, gdy stan aplikacji naprawdę je przerośnie. Użyte z tym osądem, React i Redux są znakomite dla prawdziwych aplikacji klienta — a dyscyplina wiedzy, kiedy po nie nie sięgać, jest tak cenna jak wiedza, jak.