MobX

mobx-logo

Wat is MobX?

MobX is een State Management Tool net als Redux.

MobX omschrijft zichzelf als volgt

MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP).

Anything that can be derived from the application state, should be derived.

Automatically.

- MobX

Voordat ik code laat zien...

De code voorbeelden gebruiken de volgende technieken die wij hier bij Label A niet gebruiken

  • Decorators
  • Observer Pattern
  • Composition Pattern

Decorators

In het kort zijn Decorators gewoon "fancy" mixins / Higher Order Functions (HOC). De volgende code doet hetzelfde.

HOC nu


                  class User {
                    name = 'Sander';
                    age = 27;
                  }

                  withSomeHoc(User);
                

HOC als decorator


                  @withSomeHoc
                  class User {
                    name = 'Sander';
                    age = 27;
                  }
                

Observer Pattern

Een patroon waarbij je een "observable" en meerdere "observers" hebt. De "observers" subscriben zichzelf aan een "observable" zodat zij ingelicht worden wanneer de "observable" wordt geüpdated.

                // an observable class
                class DJ {
                  currentSong = { artist: 'Frans Duijts', title: 'Samen op het strand' };
                  subscribers = [];

                  subscribe = (observer) => {
                    this.subscribers.push(observer);
                  }

                  onSongChange = () => {
                    this.subscribers.forEach(observer => {
                      observer.update(this.currentSong);
                    });
                  }
                }
              

                const dj = new DJ();

                // an observer class
                class Partygoer {
                  constructor() {
                    // subscribe to DJ
                    dj.subscribe(this);
                  }

                  update = (song) => {
                    console.log(song);
                  }
                }
              

Observer Pattern

Een patroon waarbij je een class of functie uitbreidt met waardes en functies. Hierover later in de presentatie meer + voorbeeld.

mobx en mobx-react API

De belangrijkste MobX API decorators

  • @observable
  • @observer
  • @action
  • @computed
  • @inject

@observable

Observable values kunnen JS primitives, references, plain objects, class instances, arrays en maps zijn. MobX zal de decorated value controleren op veranderingen. Met React betekent dat er een re-render zal plaatsvinden wanneer deze value verandert.

            class Counter extends Component {
              @observable count = 0;
            }
          

@observer

De @observer decorator wordt gebruikt om een React component te veranderen in een Reactive Component. Data dat gebruikt wordt voor het renderen van dit component zal dit component forceren te re-renderen wanneer deze data verandert.

            @observer
            class Counter extends Component {
              @observable count = 0;
            }
          

@action

Alles dat de state aanpast is een Action. Bij MobX gebruiken we @action om duidelijk te maken wat een action is en waar ze staan. Dit helpt om je code goed te structureren. Actions zullen mutaties samenvoegen en alleen computed values en reactions op de hoogte brengen wanneer alle andere actions zijn afgerond. Dit zorgt er voor dat values die nog bezig zijn met updaten niet zichtbaar zijn voor de rest van de applicatie tot de action is voldaan.

            @observer
            class Counter extends Component {
              @observable count = 0;

              @action
              countUp = () => {
                this.count += 1;
              }
            }
          

@computed

Computed values zijn values die afkomstig zijn van de bestaande state of een andere berekende (computed) value. Computed values moeten niet worden onderschat volgens MobX. Het deel van jouw state dat mag veranderen worden hiermee zo klein mogelijk gehouden. Computed values zijn geoptimaliseerd, dus je moet ze zo vaak mogelijk gebruiken.

            @observer
            class Counter extends Component {
              @observable count = 0;

              @action
              countUp = () => {
                this.count += 1;
              }

              @computed
              isCountDone = () => {
                return this.count === 10;
              }
            }
          

@inject

De @inject decorator gebruik je om data van MobX stores toe te voegen aan een React component. Dit is vergelijkbaar met connect van Redux.

            @inject('countStore')
            @observer
            class Counter extends Component {
              render() {
                return (
                  <p>count: {this.props.countStore.count}</p>
                );
              }
            }
          

setState?

Met MobX hoef je dus ook geen setState meer te gebruiken. Observables vervangen de state van React en geven een performance boost!

            @observer
            class Counter extends Component {
              @observable count = 0;

              @action
              countUp = () => {
                this.count += 1;
              }

              render() {
                return (
                  <div>
                    <p>count: {this.count}</p>
                    <button onClick={this.countUp}>
                      count up!
                    </button>
                  </div>
                );
              }
            }
          

Wat zijn de verschillen met Redux?

Redux

MobX

De verschillen

Redux
  • Single store
  • Plain objects
  • Immutable
  • Normalized state
MobX
  • Multiple stores
  • Observable data
  • Mutable
  • Nested state

Single store vs. Multiple stores

Redux


                // playlist
                const initialState = {
                  name = 'Funky songs';
                };

                const reducer1 = (state = initialState, action) => {
                  switch (action) {
                    default: return state;
                  }
                }

                // user
                const initialState = {
                  name = 'Sander';
                }

                const reducer2 = (state = initialState, action) => {
                  switch (action) {
                    default: return state;
                  }
                }

                const reducers = combineReducers({ reducer1, reducer2 });
                const store = createStore(reducers);
              

MobX


                class PlaylistStore {
                  name = 'Funky songs';
                }

                class UserStore {
                  name = 'Sander';
                }

                const stores = {
                  userStore: new UserStore(),
                  playlistStore: new PlaylistStore(),
                };
              

Actions

Redux

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.

            const toggleTodo = (done) => ({
              type: TOGGLE_TODO,
              done,
            })
          

MobX

Events invoke actions. Actions are the only thing that modify state and may have other side effects.

            @action
            onClick = (done) => {
              this.done = done
            }
          
Redux
  • Zelf updates nagaan
  • Expliciet
  • Passief
MobX
  • Gaat automatisch updates na
  • Impliciet
  • Reactief
Redux
  • Read-only state
  • prevState => newState
  • Pure
MobX
  • Read AND write to state
  • state => state
  • Impure
Redux
  • Actions verplicht

                const toggleTodo = (done) => ({
                  type: TOGGLE_TODO,
                  done,
                })
              

                const reducer = (state, action) => {
                  switch (action.type) {
                    case TOGGLE_TODO:
                      return {
                        ...state,
                        done: action.done,
                      };
                  }
                }
              
MobX
  • Actions niet verplicht

                <button
                  onClick={() => this.props.todoStore.done = true}
                />
              
Redux
  • Normalize state

                {
                  messages: {
                    byId: {
                      message1: {
                        id: 'message1',
                        author: 'user1',
                        body: '...',
                      },
                      /* ... more messages */
                    },
                    allIds: ['message1', /* ... */]
                  },
                  users: {
                    byId: {
                      user1: {
                        id: 'user1',
                        name: 'user1',
                      },
                      /* ... more users */
                    },
                    allIds: ['user1', /* ... */]
                  }
                }
              
MobX
  • Normalize state hoeft niet

                @observable messages = [
                  {
                    id: 'message1',
                    author: 'user1',
                    body: '...'.
                  },
                  /* ... more messages */
                ];

                @observable users = [
                  {
                    id: 'user1',
                    author: 'user1',
                  },
                  /* ... more users */
                ];
              

Code vergelijken

Opzet

Redux

                      import thunk from 'redux-thunk';
                      import todo from 'ducks/todo';

                      const middleware = applyMiddleware(thunk);
                      const reducers = combineReducers({ todo });
                      const store = createStore(reducers, middleware);
                    

                      <Provider store={store}>
                        <App />
                      </Provider>
                    
MobX

                      import todoStore from 'stores/TodoStore';

                      const stores = { todoStore };
                    

                      <Provider {...stores}>
                        <App />
                      </Provider>
                    

Segmenten

Redux

                      const TOGGLE_TODO = 'todo/TOGGLE';

                      const initialState = {
                        done: false,
                      };

                      export const reducer = (state = initialState, action) => {
                        switch(action.type) {
                        case TOGGLE_TODO:
                          return {
                            ...state,
                            done: action.done,
                          }
                        default:
                          return state;
                        }
                      }

                      export const toggleTodoAction = (done) => ({
                        type: TOGGLE_TODO,
                        done
                      });
                    
MobX

                      class TodoStore {
                        @observable done = false;

                        @action
                        toggleTodo = (done) => {
                          this.done = done;
                        }
                      }
                    

Async

Redux

                      const FETCH_ALL_TODOS_LOADING = 'todo/FETCH_ALL_LOADING';
                      const FETCH_ALL_TODOS_SUCCESS = 'todo/FETCH_ALL_SUCCESS';
                      const FETCH_ALL_TODOS_FAILED = 'todo/FETCH_ALL_FAILED';

                      const initialState = {
                        data: [],
                        loading: false,
                        error: false,
                      };

                      export const reducer = (state = initialState, action) => {
                        switch(action.type) {
                        case FETCH_ALL_TODOS_LOADING:
                          return {
                            ...state,
                            loading: true,
                            error: false,
                          };
                        case FETCH_ALL_TODOS_SUCCESS:
                            return {
                              ...state,
                              loading: false,
                              error: false,
                              data: action.payload,
                            };
                        case FETCH_ALL_TODOS_FAILED:
                            return {
                              ...state,
                              loading: false,
                              error: true,
                            };
                        default:
                          return state;
                        }
                      }

                      export const fetchAllTodos = () => async (dispatch) => {
                        dispatch({ type: FETCH_ALL_TODOS_LOADING });

                        try {
                          const response = await fetch('someapi.com/todos');
                          const data = await response.json();
                          dispatch({ type: FETCH_ALL_TODOS_SUCCESS, payload: data });
                        } catch (e) {
                          dispatch({ type: FETCH_ALL_TODOS_FAILED });
                        }
                      };
                    
MobX

                        class TodoStore {
                          @observable data = [];
                          @observable loading = false;
                          @observable error = false;

                          @action
                          fetchAllTodos = async () => {
                            this.loading = true;

                            try {
                              const response = await fetch('someapi.com/todos');
                              const data = await response.json();
                              this.data = data;
                              this.loading = false;
                            } catch (e) {
                              this.loading = false;
                              this.error = true;
                            }
                          }
                        }
                      

                        @asyncStore
                        class TodoStore {
                          @observable data = [];

                          @action
                          fetchAllTodos = () => {
                            this.data = this.api.get('someapi.com/todos');
                          }
                        }
                      

                            function asyncStore(store) {
                              return class extends store {
                                @observable loading = false;
                                @observable error = false;

                                api = {
                                  get: (path) => {
                                    this.loading = true;

                                    try {
                                      const response = await fetch(path);
                                      const data =  await response.json();

                                      this.loading = false;
                                      this.error = false;

                                      return data;
                                    } catch (e) {
                                      this.loading = false;
                                      this.error = true;
                                    }
                                  }
                                }
                              }
                            }
                          

Koppelen aan React

Redux

                      import { connect } from 'react-redux';
                      import { toggleTodoAction } from 'ducks/todo';

                      const TodoItem = (props) => (
                        <button onClick={() => props.toggleTodoAction(true)}>
                          Done!
                        </button>
                      );

                      export default connect(state => ({
                        todo: state.todo
                      }), { toggleTodoAction })(TodoItem);
                    
MobX

                        import { observer, inject } from 'mobx-react';

                        @inject('todoStore')
                        @observer
                        class TodoItem extends React.Component {
                          render() {
                            return (
                              <button onClick={() => this.props.toggleTodo(true)}>
                                Done!
                              </button>
                            );
                          }
                        }
                      

                        import { observer, inject } from 'mobx-react';

                        const TodoItem = inject('todoStore')(observer((props) => (
                          <button onClick={() => props.toggleTodo(true)}>
                            Done!
                          </button>
                        ));
                      

Redux vs. MobX

Learning Curve

Redux

"Nieuwe" FP technieken

Geen "magie"

MobX

Bekende OOP technieken

Meer "magie"

Boilerplate

Redux

Simpelere, kleinere syntax

Gebouwd met abstracties

Meer "magie"

MobX

Meer expliciet, meer syntax

Heeft meer "speciale" tools nodig (Thunk)

Geen "magie"

Developer Tools

Redux

Redux dev tools

Time Travel door immutability

MobX

Geen vervanger voor Redux dev tools

Geen Time Travel

Debuggability

Redux

Expliciet

One-way

Geen "magie"

MobX

Impliciet

Many-ways

Meer "magie"

Voorspelbaarheid

Redux

Expliciet

One-way

Pure

MobX

Impliciet

Many-ways

Impure

Modulariteit

Redux

Globaal gedeelde state

Encapsulation is niet strict

MobX

Duidelijke verdeling dmv meerdere stores

Toegang tot OOP patterns

Data/logica is encapsulated (omhult)

Schaalbaarheid / onderhoudbaarheid

Redux

Pure

Stricte orde van handelingen

Mutaties zijn gecentraliseerd

MobX

Impure

Geen stricte orde van handelingen

Mutaties kunnen overal gebeuren

Conclusie