Spis treści:
Zarządzanie stanem w React
Ktoś stworzył aplikację z przyciskiem. Wciskasz go i za każdym razem wyświetla się inna wartość. Pewnie zastanawiasz się, co dzieje się „pod spodem”. To albo czary, albo… zarządzanie stanem aplikacji. Tym razem zajmiemy się tym drugim tematem. Zarządzanie stanem w React to proces kontrolowania i aktualizowania danych w aplikacji.
Jest jednym z kluczowych zagadnień przy pracy z biblioteką programowania JavaScript. Stan to taka wartość, która zmienia się na przykład na skutek interakcji użytkownika z aplikacją. W React stan zazwyczaj przechowywany jest w komponentach, a jego zmiana może powodować ponowne renderowanie komponentu lub całego drzewa komponentów. Zarządzanie stanem jest dosyć istotne. Pozwala nam tworzyć dynamiczne i interaktywne aplikacje – dzięki temu aplikacja może:
- dostosowywać się do zmieniającego się stanu,
- reagować na interakcję użytkownika,
- wykonywać pewne akcje w odpowiednich momentach.
Jest to ważne w przypadku złożonych aplikacji, w których wiele komponentów zależy od stanów w innych komponentach.
Czym jest Redux i jak działa?
Zapewne większość osób zaznajomionych z React, słysząc o zarządzaniu stanem, przed oczami ma tylko jedno – Redux.
Redux jest biblioteką służącą do zarządzania stanem aplikacji w React. Jest to narzędzie, które umożliwia tworzenie bardziej przewidywalnych i łatwiejszych do testowania aplikacji poprzez uproszczenie zarządzania stanem i zachowywania go w jednym miejscu. Redux wprowadza wiele pojęć, które pozwalają na bardziej efektywne i przewidywalne zarządzanie stanem. Jednym z nich jest jednostronny przepływ danych. Stan aplikacji jest przechowywany w jednym centralnym magazynie, tzw. store. Dane między store a komponentami przepływają w odpowiedni sposób, zgodny z architekturą Flux. Zmiany w stanie są dokonywane tylko za pomocą akcji, czyli prostych obiektów, które opisują zmiany, jakie mają być wykonane w stanie aplikacji.
Warto również wspomnieć, że Redux może działać niezależnie od Reacta. Biblioteka ta jest uniwersalna i może być używana w innych aplikacjach internetowych. Jeśli ktoś jednak poszukuje nieskomplikowanego, intuicyjnego narzędzia, to warto poznać też inne dostępne opcje.
import React from "react"; import { Provider, useDispatch, useSelector } from "react-redux"; import { createStore } from "redux"; const incrementAction = () => ({ type: "INCREMENT" }); const decrementAction = () => ({ type: "DECREMENT" }); const counterReducer = (state = 0, action) => { switch (action.type) { case "INCREMENT": return state + 1; case "DECREMENT": return state - 1; default: return state; } }; const store = createStore(counterReducer); const Counter = () => { const count = useSelector((state) => state); const dispatch = useDispatch(); return ( <div> <div>Count: {count}</div> <button onClick={() => dispatch(incrementAction())}>+</button> <button onClick={() => dispatch(decrementAction())}>-</button> </div> ); }; const CounterApp = () => { return ( <Provider store={store}> <Counter /> </Provider> ); };
Wykorzystanie innych rozwiązań
Na szczęście istnieją różne sposoby zarządzania stanem aplikacji w React:
Context API
To wbudowane w React narzędzie do przekazywania danych między komponentami bez konieczności przekazywania ich przez „propsy” (właściwości, ang. properties). Dzięki temu można przekazywać dane do komponentów znajdujących się niżej w hierarchii bezpośrednio z góry. Można to wykorzystać do zarządzania stanem aplikacji, przekazując stan do całej aplikacji lub tylko do części komponentów.
import React, { createContext, useContext, useState } from "react"; const CounterContext = createContext(); const CounterProvider = ({ children }) => { const [counter, setCounter] = useState(0); const increment = () => { setCounter((prevCounter) => prevCounter + 1); }; const decrement = () => { setCounter((prevCounter) => prevCounter - 1); }; return ( <CounterContext.Provider value={{ counter, increment, decrement }}> {children} </CounterContext.Provider> ); }; const CounterDisplay = () => { const { counter } = useContext(CounterContext); return <div>Counter: {counter}</div>; }; const CounterButtons = () => { const { increment, decrement } = useContext(CounterContext); return ( <div> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> ); }; const CounterApp = () => { return ( <CounterProvider> <CounterDisplay /> <CounterButtons /> </CounterProvider> ); };
MobX
To biblioteka, która umożliwia łatwe zarządzanie stanem aplikacji w React. W przeciwieństwie do Redux – MobX jest bardziej elastyczny i wymaga mniej kodu. Używa się go do przechowywania stanu w obiektach, które reprezentują dane aplikacji. MobX automatycznie aktualizuje widok, gdy dane zmienią się w magazynie.
import React from "react"; import { observable } from "mobx"; import { observer } from "mobx-react"; const counterStore = observable({ counter: 0, increment() { this.counter++; }, decrement() { this.counter--; }, }); const CounterDisplay = observer(() => <div>Counter: {counterStore.counter}</div>); const CounterButtons = observer(() => ( <div> <button onClick={() => counterStore.increment()}>+</button> <button onClick={() => counterStore.decrement()}>-</button> </div> )); const CounterApp = () => { return ( <div> <CounterDisplay /> <CounterButtons /> </div> ); };
RxJS
To biblioteka reaktywna do zarządzania asynchronicznymi strumieniami danych. Można jej używać do zarządzania stanem w aplikacji. RxJS pozwala na łatwe reagowanie na zmiany w danych i wykonywanie akcji na podstawie tych zmian.
import React, { useState, useEffect } from "react"; import { Subject } from "rxjs"; import { scan } from "rxjs/operators"; const counterSubject = new Subject(); const counter$ = counterSubject.pipe( scan((count, operation) => { if (operation === "+") { return count + 1; } else if (operation === "-") { return count - 1; } else { return count; } }, 0) ); const CounterDisplay = () => { const [counter, setCounter] = useState(0); useEffect(() => { const subscription = counter$.subscribe(setCounter); return () => subscription.unsubscribe(); }, []); return <h1>Counter: {counter}</h1>; }; const CounterButtons = () => { const handleButtonClick = (operation) => { counterSubject.next(operation); }; return ( <div> <button onClick={() => handleButtonClick("+")}>+</button> <button onClick={() => handleButtonClick("-")}>-</button> </div> ); }; const CounterApp = () => { return ( <div> <CounterDisplay /> <CounterButtons /> </div> ); };
Recoil
Recoil to biblioteka, która działa poprzez przechowywanie stanu aplikacji w tzw. atomach. Atomy są prostymi obiektami, które zawierają aktualną wartość stanu oraz funkcje, które pozwalają na aktualizację wartości. Dzięki temu łatwo jest zarządzać stanem aplikacji i aktualizować go w reakcji na interakcję podejmowaną przez użytkownika lub inne zdarzenia.
import React from "react"; import { useRecoilState, atom, RecoilRoot } from "recoil"; const counterState = atom({ key: "counterState", default: 0, }); const CounterDisplay = () => { const [counter] = useRecoilState(counterState); return <div>Counter: {counter}</div>; } const CounterButtons = () => { const [counter, setCounter] = useRecoilState(counterState); const increment = () => { setCounter(counter + 1); }; const decrement = () => { setCounter(counter - 1); }; return ( <div> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> ); } const CounterApp = () => { return ( <RecoilRoot> <CounterDisplay /> <CounterButtons /> </RecoilRoot> ); };
Zustand
Zustand to lekka biblioteka do zarządzania stanem w React, która wykorzystuje podstawowe funkcjonalności React do przechowywania i aktualizowania stanu. Oferuje proste API i pozwala na tworzenie globalnego stanu, zarządzanie stanem w komponentach, własnych modułach, tworzenie selektorów i subskrypcji stanu. Jest szybka i wydajna, co sprawia, że działa dobrze nawet w dużych aplikacjach.
import React from "react"; import create from "zustand"; const useCounterStore = create((set) => ({ counter: 0, incrementCounter: () => set((state) => ({ counter: state.counter + 1 })), decrementCounter: () => set((state) => ({ counter: state.counter - 1 })), })); const CounterDisplay = () => { const counter = useCounterStore((state) => state.counter); return <div>Counter: {counter}</div>; } const CounterButtons = () => { const { incrementCounter, decrementCounter } = useCounterStore(); return ( <div> <button onClick={incrementCounter}>+</button> <button onClick={decrementCounter}>-</button> </div> ); } const CounterApp = () => { return ( <div> <CounterDisplay /> <CounterButtons /> </div> ); };
Które rozwiązanie wybrać?
Wybór rozwiązania do zarządzania stanem w React zależy od wielu czynników, takich jak rozmiar i złożoność projektu, preferencje zespołu programistycznego, doświadczenia z innymi bibliotekami i podejściami. Większość starszych projektów w React korzysta z biblioteki Redux, gdyż było to najpopularniejsze rozwiązanie, które przyzwyczaiło programistów do swojego sposobu działania. Redux jest bardzo popularny wśród większych projektów i ma duże wsparcie społeczności, co może pomóc w rozwiązywaniu problemów – nie jest jednak polecany w nowych projektach.
Według mnie, jeśli rozpoczynasz nowy projekt w React i potrzebujesz rozwiązania do zarządzania stanem, to sugerowałbym rozważenie Recoil lub Zustand. Są to biblioteki, które zostały opracowane z myślą o prostocie i łatwości użycia, co może być szczególnie ważne w nowym projekcie, w którym chcesz skupić się na szybkim wdrożeniu funkcjonalności i ograniczeniu ilości kodu. Recoil i Zustand pozwalają na tworzenie stanu przy użyciu hooków, co ułatwia integrację z React i zmniejsza potrzebę tworzenia dodatkowych plików. W przypadku Recoil możesz również korzystać z selektorów, które pozwalają na pobieranie tylko niezbędnych danych ze stanu, co może przyspieszyć renderowanie aplikacji.
Zarządzanie stanem aplikacji – podsumowanie
Podsumowując, wybór biblioteki do zarządzania stanem w React powinien być solidnie przeanalizowany i nie jest łatwo odpowiedzieć na pytanie „które narzędzie wybrać?”. Jeśli priorytetem są łatwość użycia i szybka implementacja funkcjonalności, dobrym pomysłem może być wykorzystanie któregoś z innych rozwiązań – na przykład Recoil lub Zustand.
Jeśli zastanawiasz się nad wyborem Reduxa, to warto jest przeanalizować, czy projekt jest na tyle złożony i wymagający, że jego zastosowanie jest potrzebne. Może się też okazać, że projekt nie wymaga użycia żadnej biblioteki do zarządzania stanem, ponieważ sam React dostarcza narzędzi do zarządzania stanem w komponentach, które są wystarczające dla prostych projektów