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