W poprzednim moim wpisie przedstawiłem podstawy MobX czyli alternatywnego do Redux podejścia do zarządzania stanem aplikacji ReactJS. Dziś kontynuuję ten temat przedstawiając połączenie MobX - routing ReactJS. Wiem, że temat ten dość często pojawiał się na różnego rodzaju forach dyskusyjnych i wydaje mi się, że warto go wyjaśnić w formie wpisu na blogu. Nic się nie martw - to na prawdę nic skomplikowanego!

MobX - routing ReactJS: rozwiązanie w stylu Redux

W moim wpisie na temat podstaw Redux wspomniałem co nieco o funkcji connect oraz komponencie Provider. Przypomnijmy sobie część kodu przykładu z tamtego wpisu:

import { createStore } from 'redux';
import { connect, Provider } from 'react-redux';

const store = createStore(...);

... // the rest of the example in the post about Redux

const mapStateToProps = (state) => {
  return { counter: state.counter };
};
const mapDispatchToProps = (dispatch) => {
  return {
    onIncrement: () => dispatch({ type: 'INCREMENT' }),
    onDecrement: () => dispatch({ type: 'DECREMENT' })
  }
};

Counter = connect(mapStateToProps, mapDispatchToProps)(Counter);

render(
  <Provider store={store}>
    <Counter />
  </Provider>,
  document.getElementById('root')
);

Zwróć uwagę, że tutaj przytaczam tylko kawałek tamtego przykładu. Jeśli chcesz, sprawdź całość we wspomnianym artykule.

Funkcja connect oraz komponent Provider Reduxa

Najważniejsze w powyższym przykładzie jest to, że aby połączyć komponent ReactJS ze “store” Reduxa musimy użyć funkcji connect. Ponadto musimy “owinąć” główny komponent aplikacji komponentem Provider dostarczanym przez Redux. Jest to niezbędne do tego aby “store” Reduxa był dostępny dla wywołań funkcji connect.

Chyba nie wspominałem o tym jeszcze na blogu ale aby użyć Reduxa w aplikacji ReactJS wykorzystującej bibliotekę react-router, musimy zwykle zrobić to samo z konfiguracją routingu. Oznacza to, że musimy “owinąć” tę konfigurację komponentem Provider. Zresztą spójrz na poniższy przykład:

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Route path="/" component={App}>
      </Route>
    </Router>
  </Provider>,
  document.getElementById('app')
);

Jak widzisz, konfiguracja routingu (komponent Router) “owinięta” jest przez komponent Provider Reduxa. W ten sposób komponenty ReactJS powiązane z poszczególnymi “routami” mają również dostęp do “store”.

MobX - komponent Provider

No dobra, wiesz już jak sprawa wygląda w przypadku Reduxa. Pytanie teraz jak to samo zrobić na linii MobX - routing ReactJS?

Wiesz już z poprzedniego wpisu, że w MobX nie ma czegoś takiego jak funkcja connect. Jak więc wstrzyknąć obiekt “store” do komponentów powiązanych z routingiem? Cóż, na szczęście biblioteka MobX dostarcza nam swoją wersję komponentu Provider. Poniżej objaśnienie czym jest ten komponent (z dokumentacji projektu):

Provider is a component that can pass stores (or other stuff) using React’s context mechanism to child components. This is useful if you have things that you don’t want to pass through multiple layers of components explicitly.

Czyli, że komponent Provider pozwala na przekazywanie, za pomocą kontekstu ReactJS, obiektu “store” jak i “innych rzeczy” do komponentów, które “wrappuje”. Dzięki temu konfiguracja MobX z routingiem ReactJS (przynajmniej ta jej część) będzie bardzo podobna do tej z wykorzystaniem Reduxa:

import { Provider } from 'mobx-react';
import usersStore from './stores/usersStore';
import itemsStore from './stores/itemsStore';

const stores = { usersStore, itemsStore };

ReactDOM.render(
  <Provider {...stores}>
    <Router history={history}>
      <Route path="/" component={App}>
      </Route>
    </Router>
  </Provider>,
  document.getElementById('app')
);

Zwróć uwagę, że na początku zwyczajnie łączę wszystkie obiekty “store” w jeden obiekt. Następnie przekazuję go jako argument komponentu Provider używając operatora “spread”. Wspominałem już o nim kilkakrotnie na blogu… Generalnie poniższy zapis…

<Provider {...stores}>

…jest tożsamy z takim zapisem:

<Provider userStore={userStore} itemsStore={itemsStore}>

MobX - dekorator @inject

Ale to nie wszystko. Biblioteka MobX dostarcza dodatkowo specjalny dekorator @inject. Poniżej jego definicja:

… inject can be used to pick up those stores. It is a higher order component that takes a list of strings and makes those stores available to the wrapped component.

Jak więc widzisz, dekorator ten pozwala na wybór “storów” przekazanych do komponentu Provider. Przekazuje mu się listę nazw “storów”, które mają być dostępne dla komponentu. Dla lepszego zrozumienia, spójrz na przykład komponentu ReactJS udekorowanego przez dekorator @inject:

class App extends React.Component {
  render () {
    return <ChildComponent />
  }
}

@inject('itemsStore') @observer
class ChildComponent extends React.Component {
  render() {
    return (
      <div className="index">
        {this.props.itemsStore.items.map((item, index) => {
          return <span key={index}>item.name</span>
        })}
      </div>
    );
  }
}

W powyższym przykładzie użyłem dekoratora @inject i przekazałem mu nazwę “itemsStore” jako parametr. Jak już wcześniej widziałeś w jednym z poprzednich przykładów, do komponentu Provider przekazałem obiekt, który grupował kilka różnych obiektów “store”. Każdemu z nich odpowiada właściwość tego wspólnego obiektu. I to właśnie nazwę tej właściwości przekazuję do dekoratora.

Jeśli spojrzysz teraz na metodę render komponentu ChildComponent, zauważysz, że this.props zawiera obiekt itemsStore. To właśnie dzięki użyciu dekoratora @inject i przekazaniu mu nazwy tego konkretnego obiektu “store”. Jeśli chcesz, możesz we własnym zakresie sprawdzić, że “propsy” nie zawierają w tym momencie drugiego z obiektów “store” zgrupowanych w obiekcie przekazanym do komponentu Provider.

Zwróć też uwagę na jeszcze jedną rzecz. Kolejność dekoratorów nadawanych komponentowi ma znaczenie. Dlatego też zawsze dekorator @inject powinien być tym “zewnętrznym” a @observer “wewnętrznym”!

MobX - routing ReactJS: bezpośredni import stanu

Poza opisanym powyżej sposobem na połączenie MobX - routing ReactJS istnieje jeszcze jedno podejście, w którym pomija się wykorzystanie komponentu Provider:

ReactDOM.render(
  <Router history={history}>
    <Route path="/" component={App}>
    </Route>
  </Router>
 document.getElementById('app')
);

W przypadku kiedy zrezygnujesz z użycia komponentu Provider, nie możesz też użyć dekoratora @inject w celu wyboru i wstrzyknięcia odpowiedniego obiektu “store” do “propsów” komponentu. Wtedy to musisz po prostu zaimportować ten obiekt w module komponentu:

import itemsStore from './stores/itemsStore';

@observer
class App extends React.Component {
  render() {
    return (
      <ChildComponent itemsStore={itemsStore} />
    );
  }
}

@observer
const ChildComponent = props => {
  render() {
    return (
      <div className="index">
        {props.itemsStore.items.map((item, index) => {
          return <span key={index}>item.name</span>
        })}
      </div>
    );
  }
}

Osobiście preferuję pierwsze przedstawione dziś podejście jednak drugie z nich również może być dobrym sposobem na połączenie MobX i routingu ReactJS.

Jeśli zdecydujesz się na wykorzystanie MobX bez użycia komponentu Provider, powinieneś pamiętać o poważnym potraktowaniu podejścia z podziałem aplikacji ReactJS na kontenery i komponenty prezentacyjne. Zresztą zawsze powinieneś traktować je poważnie… W każdym razie w takim przypadku, tak jak pokazałem na powyższym przykładzie, import odpowiednich obiektów “store” powinien się odbywać w kontenerze. Tak jak już kiedyś pisałem na temat podziału komponentów ReactJS, to kontener jest odpowiedzialny za wszelką logikę, jest więc właściwym miejscem na manipulacje obiektem “store”.

Podsumowanie

Przedstawiłem dziś dwa podejścia do użycia biblioteki MobX w aplikacji ReactJS wykorzystującej routing. Mam nadzieję, że będzie to dla Ciebie przydatne…

Pierwsze z podejść jest całkiem podobne do tego jaki stosujemy przy wykorzystaniu Reduxa. Jest to preferowane przeze mnie podejście jednak nie uważam, że drugie z nich jest złe. Jak zwykle w programowaniu… wszystko zależy od potrzeb i wymagań projektu.