Redux vs Mobx vs RxJS

15 хв. читання

Обговорюючи дата-менеджент бібліотеки веб-застосунків з React, частіше за все ми згадуємо дві назви: Redux та MobX. Можна, заради цікавості, додати сюди ще й RxJS.

Я вирішив порівняти ці технології, створивши за допомогою Redux, MobX та RxJS три ідентичні за задумом застосунки для відстежування погоди, так званий WeatherApp. Лежить ось тут: https://github.com/georgeshevtsov/redux-rxjs-mobx-comparison.

Redux vs MobX

Оскільки RxJS — це взагалі стиль життя, а не підхід до менеджменту даних у застосунку, я залишу його на кінець. А зараз я б хотів звернути увагу на збіжності й розбіжності між Redux і MobX. Якщо ви вже маєте досвід з Redux та MobX, прогортайте до RxJS.

Розглянемо способи підключення Redux і MobX на рутовому рівні.

Redux рут

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';

import App from './components/app';
import reducers from './reducers';
import promiseMiddleware from 'redux-promise';

const createStoreWithMiddleware = applyMiddleware(promiseMiddleware)(createStore);

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}>
    <App />
  </Provider>, document.querySelector('.container'));
```	

Redux дає нам можливість використовувати *store — єдине джерело правди*. Тільки він містить актуальний зразок даних, лише через нього можливо внести зміни до даних. Store — загальне сховище даних різних типів і призначень. В цьому прикладі store створюється з переданих редюсерів. Для реалізації та обробки асинхронного запиту на сервіс [openWeatherMap](https://openweathermap.org/api) в застосунку потрібно використати **middleware** (прошарок) [redux-promise](https://www.npmjs.com/package/redux-promise) він відловить екшн, що має проміс в параметрі пейлоуд, дочекається виконання промісу і відправить екшн з отриманими даними в редюсер. На відміну від MobX, ми ще не можемо напряму користуватися даними з обгортки в компонентах, але до цього перейдемо пізніше. 

#### **MobX рут**

```javascript
import React from 'react';
import { observable } from 'mobx';
import { observer, Provider } from 'mobx-react';

const temp = observable(new Temperature())

ReactDOM.render(
  <Provider temperature={temp}>
    <App />
  </Provider>,
  document.getElementById("root")
)

В першому і в другому руті є обгортка у вигляді провайдера. У Redux екземплярі застосунку, в провайдер передається store через props, а у MobX прикладі екземпляр класу температури temp. temp загортається в функцію observable, що дає MobX зрозуміти, що потрібно відстежувати зміни в значеннях відповідних параметрів класу. Обгортка observer, що огортає сам компонент App, надає функціонал оновлення компонентів у відповідь на зміни в зазначеному класі температури.

До речі в MobX з коробки немає можливості користуванням Redux-подібним store, простіше кажучи загальним сховищем даних, проте є сторонній модуль що надає такий функціонал mobx-state-tree.

Redux action

import axios from 'axios';
export const FETCH_WEATHER = 'FETCH_WEATHER';

const APPID = 'you need to do it yourself';
const getWeatherUrl = location => `https://api.openweathermap.org/data/2.5/weather?appid=${APPID}&q=${location}&units=metric`;

export function fetchWeather(location) {
    return {
        type: FETCH_WEATHER,
        payload: axios(getWeatherUrl(location))
    }
}

В екшні я використав axios в ролі бібліотеки для виконання запитів. Функція екшну аргументом приймає рядок location. При виконанні екшну створюється об'єкт, з параметром payload, значенням якого буде результат виконання запиту до weather-api, а саме проміс. Мідлвер, що був підключений вище, затримує екшн до виконання промісу і дає цьому екшну пройти у редюсер, який буде реагувати на тип FETCH_WEATHER і зробить запис тіла запиту в редюсер weather.

Redux reducer

import { combineReducers } from 'redux';
import { FETCH_WEATHER } from '../actions/weatherActions';

function weatherReducer(state = {}, action) {
    switch (action.type)  {
        case FETCH_WEATHER:
            return action.payload;
        default:
            return state;
    }
}

const rootReducer = combineReducers({
    weather: weatherReducer
});

export default rootReducer;

Таким чином створюється редюсер weatherReducer, він виконує обробку всього одного типу екшнів і робить запис у свій стейт всього, що буде поміщено в action.payload.

В цьому прикладі є також частина, що об'єднує всі редюсери, вірніше їх хендлери, в один об'єкт. На його основі створюється store.

MobX Model

import {observable, computed, action } from 'mobx';

class Temperature {
  @observable temperatureCelsius = '';
  @observable location = '';
  @observable loading = false;

  @action fetch() {
     window.fetch(`https://api.openweathermap.org/data/2.5/weather?appid=${APPID}&q=${this.location}&units=metric`)
    .then(res => res.json()
    .then(action(json => {
      this.temperatureCelsius = json.main.temp;
      this.location = json.name;
      this.loading = false;
    })))
  }
  @action setLocation(city) {
    this.location = city;
    this.fetch();
  }

  @computed get temperature() {
    let tempString = this.temperatureCelsius != '' ? this.temperatureCelsius + "ºC" : ''
    return tempString;
  }
}

Мобікс-підхід, як можна помітити, — ООП. По-перше, клас використаний у ролі сховища даних та методів для їх змін. По-друге, мутабельність повсюди, на відміну від Redux підходу, де функції — чисті (на скільки це можливо), і також іммутабельність в редюсерах. Для магії Mobx в даному класі використовуються міксіни.

  • observable — що повідомляє Mobx про те, що зміни по цьому параметру потрібно відстежувати.
  • computed — декоратор обчислення, щоб зазначити, що вихідні дані можуть походити від стейту.
  • action— цей декоратор існує для того, щоб відмітити, які саме функції будуть змінювати стейт, а саме, параметри класу, загорнуті в observable міксін. MobX — має devtools, він, так як і redux-devtools, видає лог екшенів.

Redux component

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchWeather } from "../actions/weatherActions";
import { get } from 'lodash';

class App extends Component {
  state = {
     input: ''
  };
  getTemperature = (data) => {
      let temp = get(data, 'data.main.temp', false);
      return temp ? temp + '°C' : '';
  };
  getCityName = (data) => {
      return get(data, 'data.name', '')
  };
  fetchWeather = () => {
      if (this.state.input.length) {
          this.props.fetchWeather(this.state.input);
          this.setState({ input : ''})
      }
  };
  render() {
    return (
      <div>
          <input type="text" onChange={(e) => this.setState({ input: e.target.value})} value={this.state.input}/>
          <button onClick={this.fetchWeather} disabled={!this.state.input.length}>Get weather</button>
          <h2>{`${this.getCityName(this.props.weather)} ${this.getTemperature(this.props.weather)}`}</h2>
      </div>
    );
  }
}
function mapStateToProps(state) {
    return {
        weather: state.weather
    }
}
export default connect(mapStateToProps, ({ fetchWeather }))(App);

За допомогою connect ми прив'язуємо дані з редюсеру за допомогою mapStateToProps та функцію екшну в props нашого компоненту. Тепер компоненти будуть отримувати оновлення, як тільки дані в редюсері будуть змінені. Як можна побачити сервісний функціонал для користування даними з редюсера описаний в самому компоненті. Для обробки змін в інпуті, я вирішив використати внутрішній стейт компоненту, тому що немає потреби зберігати введені дані в store, демонстрація обробки запиту в умовах Redux є достатньою для цього експерименту.

MobX view

const App = observer(
  ["temperature"],
  ({ temperature }) => (
  <div>
    <TemperatureInput />
    <h2>
        {`${temperature.location} ${temperature.loading ? "loading.." : temperature.temperature}`}
      </h2>
  </div>
))

@observer(["temperature"])
class TemperatureInput extends React.Component {
  @observable input = "";
  render() {
    return (
      <div>
        <input 
          onChange={this.onChange}
          value={this.input}
        />
        <button onClick={this.onSubmit}>Get Weather</button>
      </div>
    )
  }

  @action onChange = (e) => {
    this.input = e.target.value
  }
  
  @action onSubmit = () => {
    this.props.temperature.setLocation(this.input);
    this.input = ""
  }
}

Observable — виступає чимось на кшталт connect з Redux. Дає можливість користуватися даними. Створений компонент для інпуту та його хендлерів чимось нагадує ванільне React рішення. Цікаво те, що можна позначати методи цього компоненту, як такі, що можуть змінювати дані в класі прив'язаному до цього компоненту. До того, як я почав це дослідження я думав що MobX – бібліотека, що за методами схожа на Redux, але вже зараз очевидно, що це абсолютно інша ідеологія. Перейдемо до розгляду версії застосунку з RxJS.

RxJS

Говорячи про RxJS — можна сказати, що це бібліотека для реактивного програмування на потоках — observables, якщо говорити більш людською мовою, то це гарний інструмент для взаємодії з івентами, тільки івенти тепер потрібно сприймати у вигляді потоку.

RxJS рут

import React from 'react'
import { render } from "react-dom"
import config from "recompose/rxjsObservableConfig"
import {
  setObservableConfig,
} from "recompose"

setObservableConfig(config)

render(<App />, document.getElementById("root"))

Все що потрібно для користування RxJS з Reсompose – додати конфіг у рутовому файлі. Reсompose – бібліотека компонентів вищого порядку (HOC). Я буду використовувати HOC, як засіб декомпозиції функціоналу від загального вигляду. Використовувати React з RxJS в зв'язці з Recompose необов'язково. Але мені підхід з Recompose здався більш модульним.

RxJS streams

import {
  createEventHandler,
  mapPropsStream
} from "recompose"
const fetchLocationStr = location => {
   return axios(`https://api.openweathermap.org/data/2.5/weather?appid=${APPID}&q=${location}&units=metric`)
}

const getWeather = mapPropsStream(props$ => {
   const {
      stream: onClick$,
       handler: onClick
    } = createEventHandler();

    const click$ = onClick$
    .startWith('')
    .switchMap(
      (v) => v.length 
        ? Observable
          .fromPromise(fetchLocationStr(v))
          .startWith({ data: { name: "loading..." }})
          .catch(err => Observable.of({
            data : {
              name: 'Not found...'
            }
          }))
          .pluck('data')
        : Observable.of(v)
    )
    return props$
      .switchMap(
        props => click$,
        (props, data) => ({ ...props, data, onClick, text: '' })
      )
})

const getText = mapPropsStream(props$ => {
   const {
      stream: onChange$,
      handler: onChange
    } = createEventHandler();
    const text$ = onChange$
        .map(e => e.target.value)
        .startWith('') 
    return props$.switchMap(
        props => text$,
        (props, text) => ({...props, text, onChange})
    );
});

В цьому файлі є два блоки обробки потоків один з них це потік getText, що створює та віддає функціонал івенту зміни поля вводу. Розглянемо його більш детально. mapPropsStream — функція з recompose, що дає можливість сконфігурувати потоки й передавати їх в props компонента, mapPropsStream приймає в колбек функції потік props$, (взагалі все що буде з суфіксом $ — це потік). Далі створюється потік івент хендлера та функція івент хендлера для виклику в компоненті. Далі створюється потік text$ — з потоку onChange$, за допомогою оператору map відловлюємо кожну зміну інпуту, а за допомогою оператора startsWith — встановлюємо початкове значення перед тим, як потік почне отримувати виклики.

Оператори

Для того щоб отримати результати роботи потоку, а також хендлер в props компонента використовується оператор switchMap. SwitchMap мержить потоки в один, таким чином ми отримуємо об'єкт з props, а також значенням тексту та хендлером. getWeather працює за схожою схемою. Для того, щоб зробити запит був використаний switchMap, в колбек функції є перевірка чи існує значення тексту, якщо так то створюється потік с промісу, який видобувається з запиту до weather-api, якщо тексту немає просто повертаємо потік зі значенням що є в наявності. За схожим сценарієм зі створеного потоку click$ в props мапимо дані отримані з запиту та хендлер кліку, а також в результаті кліку та запиту встановлюємо параметр text зі значенням порожнього рядка.

Компонент для створених потоків

import React from "react"
import {
  compose
} from "recompose"

class FindWeather extends React.Component {
  findWeather = () => {
    this.props.onClick(this.props.text);
  }
  getTemperature = (data) => {
    return data.main ? data.main.temp + '°C' : ''
  }
  getCityName = data => {
    return data.name || '' 
  }
  render() {
    return (
      <div>
        <input type="text" onChange={this.props.onChange} value={this.props.text}/> 
        <button onClick={this.findWeather}>Get Weather</button>
        <h2>{`${this.getCityName(this.props.data)} ${this.getTemperature(this.props.data)}`}</h2>
      </div>
    );
  }
}

const FindWeatherComposed = compose(
  getWeather,
  getText,
)(FindWeather)

const App = () => (
  <div>
    <FindWeatherComposed />
  </div>
)

В результаті в компоненті ми бачимо тільки методи викліків хендлерів з потоків описаних вище. Створення FindWeatherComposed описує механізм впровадження HOC структур за допомогою композиції що огортають компонент.

Рейтинг від простого до складного (якщо ви ніколи не використовували RxJS.)

  1. Redux / Mobx (по взаємодії з даними для мене приблизно однакові)
  2. RxJS — космос (дуже варто спробувати).

І що ж

Метою цього експерименту було дослідження відмінностей можливих підходів в сучасних React застосунках, а також закликати вас розширювати свої знання, для того щоб будувати більш якісні рішення. Після тривалого використання Redux, цікавинкою для мене став MobX не дивлячись на об'єктну орієнтованість, хочеться подивитись на реальні продуктові рішення з Mob, може навіть попрацювати. Це був беззаперечно і однозначно цікавий досвід.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.2K
Приєднався: 9 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація