Redux, jak i dedykowaną dla Reacta paczkę React Redux można uznać za najpopularniejsze w tym momencie rozwiązanie do kompleksowego zarządzania stanem w React.
Redux potrafi być pobrany ponad 900 tysięcy razy dziennie (za npm-stat), natomiast React Redux ponad 650 tysięcy razy. Nie jest jednak rozwiązaniem idealnym i ma pewne wady:
- Duża ilość powtarzalnego kodu, tzw boilerplate
- Stan jest globalny – można go modyfikować z wielu miejsc (z dowolnego miejsca można nadać akcję)
- Nieodpowiednio użyty może spowodować problemy wydajnościowe
Czy istnieją alternatywy? Tak. Nie tak popularne jak Redux, ale rozwiązujące niektóre problemy inaczej.
Alternatywy dla Reduxa
React Context
React Context to natywne API Reacta służące do przechowywania wartości potrzebnych w wielu miejscach. Powstał jako odpowiedź na tzw prop drilling, czyli sytuację przekazywania propsów przez wiele poziomów drzewa komponentów aż dotrze do tego, który danego propsa potrzebuje.
Tworzenie i używanie kontekstu
Kontekst stworzymy używając funkcji createContext()
która jako parametr przyjmuje wartości początkowe:
const ThemeContext = createContext({ theme: 'No theme set', setTheme: (value) => {console.log(value)} });
Aby pobrać wartość użyjemy hooka useContext, który jako parametr dostanie utworzony przez nas kontekst i od tej pory będzie pilnował, by za każdym razem gdy wartości kontekstu zostaną zmienione były one dostarczane do komponentu:
const ThemeInfo = () => { const { theme } = useContext( ThemeContext ); return ( <div> <p> The theme is {theme} </p> </div> ); };
Co jednak trzeba zrobić, by dać możliwość nadpisania obecnych wartości? Należy użyć Providera, który będzie dostawał nowe wartości. Same wartości natomiast mogą znajdować się w stanie komponentu w któym Provider jest użyty:
const App = () => { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }} > <ThemeInfo /> </ThemeContext.Provider> ); };
W takiej sytuacji do każdego komponentu w aplikacji zostanie przekazana funkcja setTheme
. Wywołanie jej zmieni stan w komponencie App
, wtedy Provider
dostarczy do kontekstu nowe wartości a komponenty które używają hooka useContext
te wartości dostaną.
React Context – pełny przykład
const ThemeContext = createContext({
theme: 'No theme set',
setTheme: (value) => {console.log(value)}
});
const ThemeInfo = () => {
const { theme } = useContext(
ThemeContext
);
return (
<div>
<p>
The theme is {theme}
</p>
<Buttons/>
</div>
);
};
const Buttons = () => {
const { setTheme } = useContext(
ThemeContext
);
return (
<>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
</>
)
};
const App = () => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider
value={{ theme, setTheme }}
>
<ThemeInfo />
</ThemeContext.Provider>
);
};
Zalety stosowania React Context
- Jest natywnym API Reacta
- Jest dość prosty, nie ma za dużo bolerplaite
- Nadaje się do używania w sytuacjach, gdy zmiany są rzadkie
Wady stosowania React Context
- Źle wypada w sytuacji, gdy zmiany są częste, ponieważ:
- Każda zmiana przerenderowywuje dzieci – potencjalne problemy z wydajnością
- Optymalizacja ręczna byłaby czasochłonna, biblioteki często mają zaimplementowane optymalizacje rerenderowania
MobX
MobX jest biblioteką zarządzania stanem bazującą na programowaniu reaktywnym. Jeżeli mamy jakąś istniejącą strukturę danych np klasę, tablicę czy zwykły obiekt JS, za pomocą MobX możemy dodać możliwość obserwowania i reagowania na zmiany jakie w niej zachodzą.
Przygotowanie aplikacji do użycia MobX
Po zainstalowaniu biblioteki i paczki dla Reacta poleceniami:
npm install mobx mobx-react
Możemy (nie musimy – biblioteka udostępnia API do pracy bez konieczności aktywowania dekoratorów) również włączyć obsługę dekoratorów których używa MobX w naszej aplikacji utworzonej za pomocą create-react-app. Trzeba zainstalować skrypty customize-cra które pozwolą nadpisać konfigurację Webpacka bez konieczności robienia eject
, dzięki czemu nie będziemy musieli sami zarządzać konfiguracją:
npm install customize-cra --save-dev
Następnie trzeba zainstalować plugin Babela do importu modułów:
npm install babel-plugin-import
A potem w katalogu głównym aplikacji dodać plik config-overrides.js z zawartością:
const { override, disableEsLint, addDecoratorsLegacy, fixBabelImports, } = require("customize-cra");module.exports = override( disableEsLint(), addDecoratorsLegacy(), fixBabelImports("react-app-rewire-mobx", { libraryDirectory: "", camel2DashComponentName: false }), );
Zainstalujmy skrypty które będą uruchamiać aplikację:
npm install react-app-rewired --save-dev
Aby poprawne skrypty zostały odpalone, należy w package.json
zamienić:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
Na:
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test — env=jsdom", "eject": "react-app-rewired eject" }
Użycie MobX w praktyce
Stwórzmy klasę, która będzie przechowywać stan:
import { observable } from "mobx"; class Store { @observable theme = 'No theme set'; } const store = new Store();
Jeżeli pole klasy udekorujemy za pomocą @observable
, przypisanie do niego nowej wartości poinformuje o zmianie wszystkich subskrybentów. Utwórzmy teraz klasę która zasubskrybuje do zmian:
import { observer } from "mobx-react" @observer class ThemeInfoClass extends React.Component { render() { const { store } = this.props; return ( <div> <p> The theme is {store.theme} (class) </p> <Buttons store={store}/> </div> ) }; }
Udekorowanie klasy za pomocą @observer
powoduje opakowanie metody render dodatkowym kodem, który spowoduje reakcję na zmiany. Może być to również komponent funkcyjny, wtedy jednak nie możemy użyć observer
jako dekoratora tylko jako funkcję:
const ThemeInfo = observer(({store}) => {
return (
<div>
<p>
The theme is {store.theme}
</p>
<Buttons store={store}/>
</div>
);
});
W komponencie Buttons dokonujemy natomiast przypisania nowej wartości do store:
const Buttons = ({store}) => {
return (
<>
<button onClick={() => store.theme = 'dark'}>Dark</button>
<button onClick={() => store.theme = 'light'}>Light</button>
</>
)
};
Dla uproszczenia store
przekazywany jest bezpośrednio, nic nie stoi jednak na przeszkodzie, by przekazywać store poprzez omówiony wcześniej React Context – zalecają to sami twórcy MobX.
MobX – pełny przykład
import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from "mobx-react"
import { observable } from "mobx"
@observer
class ThemeInfoClass extends React.Component {
render() {
const { store } = this.props;
return (
<div>
<p>
The theme is {store.theme} (class)
</p>
<Buttons store={store}/>
</div>
)
};
}
const ThemeInfo = observer(({store}) => {
return (
<div>
<p>
The theme is {store.theme}
</p>
<Buttons store={store}/>
</div>
);
});
class Store {
@observable theme = 'No theme set';
}
const store = new Store();
const Buttons = ({store}) => {
return (
<>
<button onClick={() => store.theme = 'dark'}>Dark</button>
<button onClick={() => store.theme = 'light'}>Light</button>
</>
)
};
const App = () => {
return (
<>
<ThemeInfo store={store} />
<ThemeInfoClass store={store} />
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Zalety stosowania MobX
- Mały boilerplate
- Renderuje tylko to, co naprawdę się zmieniło
- Nie trzeba dostosowywać do niego całej architektury aplikacji
Wady stosowania MobX
- Nie wspiera domyślnie immutability, nie jest stworzona z tą koncepcją jako główną
- Jest trudna w debugowaniu
MobX State Tree
MobX State Tree to biblioteka bazująca na MobX, która do zalety MobX jaką jest możliwość obserwowania zmian dodaje zalety związane z podejściem immutability – mimo że takiego podejścia wprost nie implementuje, stan jest mutowany ale tworzony jest z niego immutable snapshot.
Instalacja i użycie MobX State Tree
Po zainstalowaniu biblioteki poleceniami:
npm install mobx mobx-state-tree --save
Możemy utworzyć pierwszy model:
import { types } from "mobx-state-tree";
const Theme = types.model({
value: 'Not set'
});
Dzięki temu że model ma określony interfejs, przy tworzeniu instancji będzie rzucał błędem jeżeli pojawią się pola nieprzewidziane w interfejsie:
const theme = Theme.create({ value: 'No value', }); // Rzuci błąd const theme2 = Theme.create({ value: 'No value', active: false, });
Zmiana wartości modelu odbywa się poprzez akcje, które deklarujemy przy tworzeniu modelu:
const Theme =
types.model({
value: 'Not set'
})
.actions(self => {
return {
setTheme(value) {
self.value = value;
}
}
});
Samo użycie modelu MST wygląda jak użycie store z MobX – poza tym oczywiście, że aktualizujemy nazwy pól i sposób w jaki ustawiamy wartość modelu:
const Theme = types.model({ value: 'Not set' }) .actions(self => { return { setTheme(value) { self.value = value; } } }); const store = Theme.create({ value: 'No value', }); ... <p> The theme is {store.value} </p> ... <button onClick={() => store.setTheme('dark')}>Dark</button> ...
MobX State Tree – pełny przykład
import React from 'react';
import ReactDOM from 'react-dom';
import { observer } from "mobx-react";
import { types } from "mobx-state-tree";
const Theme =
types.model({
value: 'Not set'
})
.actions(self => {
return {
setTheme(value) {
self.value = value;
}
}
});
const store = Theme.create({
value: 'No value',
});
const ThemeInfo = observer(({store}) => {
return (
<div>
<p>
The theme is {store.value}
</p>
<Buttons store={store}/>
</div>
);
});
const Buttons = ({store}) => {
return (
<>
<button onClick={() => store.setTheme('dark')}>Dark</button>
<button onClick={() => store.setTheme('light')}>Light</button>
</>
)
};
const App = () => {
return (
<>
<ThemeInfo store={store} />
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Zalety stosowania MobX State Tree
- Posiada wszystko co ciekawe w Redux – akcje, selektory itd
- Wbudowana memoizacja
- Większy porządek niż w przypadku zwykłego MobX
- Łatwa możliwość zaimplementowania cofania się w czasie dzięki posiadaniu zalet podejścia immutability
Wady stosowania MobX State Tree
- Trochę boilerplate – modele, akcje itd..
Reusable
Reusable bazuje na nieco innym podejściu – umożliwia utworzenie singletona z customowego hooka z lokalnym stanem z useState
.
Instalacja i użycie Reusable
Po zainstalowaniu biblioteki poleceniem:
npm install reusable
Przekażmy do funkcji createStore
customowy hook zarządzający stanem:
// Lokalny
const useThemeHook = () => {
const [theme, setTheme] = useState('Not set');
return {
theme,
setTheme,
};
};
// Singleton
const useTheme = createStore(useThemeHook);
Wartości z hooka useTheme
są globalne. Mimo tego że odczytujemy je jak zwyczajne hooki lokalne, wartości są aktualizowane w całej aplikacji:
const { theme } = useTheme(); return ( <div> <p> The theme is {theme} </p> <Buttons/> </div> ); ... const {setTheme} = useTheme(); return ( <> <button onClick={() => setTheme('dark')}>Dark</button> <button onClick={() => setTheme('light')}>Light</button> </> )
Reusable – pełny przykład
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import {createStore, ReusableProvider} from "reusable";
// Lokalny
const useThemeHook = () => {
const [theme, setTheme] = useState('Not set');
return {
theme,
setTheme,
};
};
// Singleton
const useTheme = createStore(useThemeHook);
const ThemeInfo = () => {
const { theme } = useTheme();
return (
<div>
<p>
The theme is {theme}
</p>
<Buttons/>
</div>
);
};
const Buttons = () => {
const {setTheme} = useTheme();
return (
<>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
</>
)
};
const App = () => {
return (
<ReusableProvider>
<ThemeInfo />
</ReusableProvider>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Zalety stosowania Reusable
- Zdecydowanie najmniej kodu ze wszystkich omawianych przykładów
- Fakt globalnego stanu nie wpływa na architekturę aplikacji
Wady stosowania Reusable
- Biblioteka jest bardzo młoda, nieprzetestowana w boju.
Podsumowanie
Statystyki popularności są bezlitosne:
- React Redux: ponad 650 tysięcy pobrań
- MobX: 70 tysięcy pobrań
- MobX State Tree: poniżej 10 tysięcy pobrań
- Resuable: poniżej 200 pobrań
Redux może więc spać spokojnie, dzierżąc palmę pierwszeństwa jeśli chodzi o popularność wśród bibliotek zarządzania stanem. Dobrze jest jednak wiedzieć, że istnieją alternatywy – w jakimś projekcie można na nie trafić, ponadto mogą być fajną ciekawostką wartą wspomnienia na rozmowie kwalifikacyjnej.