Pull to refresh

Comments 28

Более красивый и лаконичный код получается писать если использовать mobx.

Для начала скажу, что MobX в проде не юзал пока, дабы был контекст. Теперь по делу. Мне нравится подход MobX лаконичностью, но там нет святого грааля Redux в моем понимании — возможности сделать replay. В идеале, хочу держать в свое сторе как можно более полное состояние приложения, чтобы потом при ошибке у пользователя получить набор экшнов, который к этому привел, и с точностью воспроизвести ошибку. Плюс самому дебажить по диффам между состояними стора и списку экшнов — одно удовольствие.
Я к курсе, что есть mobx-state-tree. Глубоко его не изучал пока что, но собираюсь посидеть и поковырять. Есть опасения насчет производительности в некоторых случаях. Как я понимаю, для того чтобы заставить императивные операции, которые мутируют стейт, возвращать новое иммутабельное состояние, они должны были запилить некий proxy наподобие immer. Есть подозрение, что в некоторых случаях редьюер сделанный руками будет эффективнее, чем иммутабельность через прокси в mobx-state-tree.

В идеале, хочу держать в свое сторе как можно более полное состояние приложения, чтобы потом при ошибке у пользователя получить набор экшнов, который к этому привел, и с точностью воспроизвести ошибку.

Часто вижу такие комментарии и понять не могу, как вы код пишете, что не понимаете, что происходит в приложении при ошибке. Вам в консоль валится полный трейс откуда ноги растут, а вы без всяких чудесных расширений для хрома, не понимаете что происходит.
Как по мне, то разница между простым логгированием и экшнами в том, что логгирование позволяет попробовать догадаться о состоянии, что там происходило, а экшны дают точную реплику этого состояния. В общем, для маленьких приложений, вроде как, и не актуально. А в средних и больших, вроде как, и полезно.
1. mobx-state-tree не нуждается в создании Proxy перед каждым изменением, как immer. Потому что все поля и методы моделей определяются статически, во время разработки.

2. Асимптотически mobx и mobx-state-tree быстрее redux за счет реактивности. Пример: список комментариев к этой статье. При изменении одного комментария, для redux количество операций это O(N), где N — кол-во connected компонентов. А для mobx это O(1) — компонент, который отображает единственный измененный комментарий. Т.е. при небольших изменениях mobx быстрее.

Зато, если мы меняем сразу много данных, то redux может выиграть за счет использования plain объектов. Потому что рендеринг каждого реактивного объекта mobx — это не только чтение данных, но и обновление графа зависимостей. В общем, it depends =)

1. Спасибо. Посмотрел. Ваша правда.
2. Вы ведь немного лукавите насчет O(n), правда? :) Я к тому, что `listener`, конечно, будет вызываться при каждом обновлении стора, но использование PureComponent позволит избежать ререндера.
Разумеется, я имею в виду N вызовов mapStateToProps. Да, reselect помогает не рендерить много. Но, часто, написать корректную мемоизацию становится нетривиальной задачей.

Справедливости ради замечу, что в React-redux v6 поменяли логику подписки и теперь есть только одна подписка на store — в провайдере. В релизном посте есть немного деталей.


Кроме того, планируется использование экспериментальных observedBits, чтобы не вызывать лишних потребителей контекста.


Утверждать, что React-redux обгонит MobX не буду, но положительные улучшения все равно есть.

Только вот че-то они пишут, что все как раз наоборот:


Both an earlier WIP v6 iteration and the final version of the v6 PR were slower than v5, in all benchmark scenarios

И еще про mobx-state-tree. Он очень облегчает жизнь, если нужна сложная логика при работе с реляционными или графовыми данными. В redux в таком случае приходится работать с их нормализованным представлением. Но если в reducer это удобно, то в компонентах, селекторах и эффектах —это боль =(


И тут нет хорошего решения:


  • Либо мы делаем большинство компонентов connected и работаем с нормализованными данными. Это порождает тонны бойлерплейта и ударяет по производительности.
  • Либо используем схемы для денормализации (redux-orm, normalizr.denormalize). Что уже несколько удобнее, но еще сильнее убивает производительность.
Можете пример какой подсказать или статью где есть работа с графовыми данными в MST и аналогично в Redux?
Сам какое-то время работал с redux-orm. Увидев, выпученные от удивления глаза новых членов команды, выплил ее к чертям. Сейчас для хранения списков, если нет глубокой вложенности, предпочитаю `Map`, т.к. оно и O(1) по ключу дает, но при этом и порядок вставки запоминает.

Честно говоря, не видел статей с таким сравнением. Пишу из личного опыта: по работе пришлось писать редактор объектного графа, в котором валидаторы сущностей залезали на два-три уровня вглубь по связям. На голом Redux быстро запутался с мемоизацией. redux-orm на сколько я помню, работает только с плоскими реляционными таблицами. Т.е. не может вот так:


type User = {
  id: number;
  addresses: [{
    region: Region; // ссылка на другую сущщность
    street: string;
    // ...
  }];
  // ...
};

А вот MST с этим справился на ура.


По функциональности createSelector из reselect похож на computed из mobx. Но есть отличия в поведении. Для примера: хотим получить объект комментария с включенным автором. На reselect будет что-то вроде:


сconst makeGetComment = () => createSelector(
  [state => state.entities.comments, state => state.entities.users,
  (state, props) => props.id],
  (comments, users, id) => ({
    ...comments[id],
    author: users[comments[id].author]
  })
)

Причем, этот селектор инвалидируется при изменении любого пользователя или коммента. И PureComponent не поможет.


А в mobx этого всего вообще не надо: у нас уже есть простая ссылка между объектами comment.author. И все зависимые вычисления (computed, rendering) будут выполнены только при изменении конкретного автора или конкретного коммента.


Но часто не мы решаем, какие технологии использовать, а они достаются нам по наследству. И Redux будет с нами еще долгие годы. Поэтому я сейчас пишу велосипед, который выполняет денормализацию со structural sharing. Как доделаю — будет статья на Хабр. Там и попробую сравнить производительность с MST.

А что если так?
```javascript
const makeGetComment = () => createSelector(
[
(state, props) => state.entities.comments[props.id],
(state, props) => state.entities.users[
state.entities.comments[props.id].author
]
],
(comment, author) => ({
...comment,
author
})
)
```
Выглядит, конечно, ужасно, но работать по идее должно без лишней инвалидации селектора. Как раз такой пример по идее и решается redux-orm. Хотя, честно говоря, не так уж часто мне приходилось сталкиваться с необходимостью делать подобную денормализацию. Обычно, проще сервер попросить вернуть данные в нужном формате. Иначе если запросить список юзеров и список комментов отдельно, то можно получить слишком много данных (не все комменты нужно отображать, не для всех юзеров и т.д.). А если предположить, что список комментов уже был получен ранее, то можно нарваться на проблемы кеширования. Так что подобные проблемы становятся действительно актуальны чаще всего тогда, когда мы хотим с локальным кешем работать и лениво го с серваком синхронизировать.
Так или иначе, ваш посыл понятен. Спасибо. Надо присмотреться к MST.

Да, так будет работать. А вот так уже нет:


const makeGetPost = () => createSelector(
  [
    (state, props) => state.entities.posts[props.id],
    (state, props) => state.entities.posts[props.id].comments
      .map(id => state.entities.comments[id])
  ],
  (post, comments) => ({
    ...post,
    comments
  })
)

Поэтому я и говорю, что запутался в селекторах.


Посмотрел, как работает redux-orm. Он явно сверяет все entity, затронутые селектором, с их предыдущими версиями. И тут уже неизвестно, что быстрее — такое сравнение или VDOM reconciliation.


не так уж часто мне приходилось сталкиваться с необходимостью делать подобную денормализацию

Да, не часто. Но если встречается, то мало никому не покажется =)


Пример из жизни:


type Region = { id, name, ... }
type RegionalTariff = { id, regions: Region[], ... }
type FederalTariff = { id, regionalTariffs: RegionalTariff[], ... }

И правило валидации: списки регионов у разных региональных тарифов не должны пересекаться в рамках федерального тарифа.

Рассмотрим типичный пример того, как можно послать AJAX запрос в Redux. Давайте представим, что нам крайне необходим список котиков с сервера.

Далее идёт 50 строчек бойлерплейта, которые собственно AJAX запроса даже не выполняют.


И к чему пришли:

16 строчек бойлерплейта, которые опять же ничерта не делают.


Смотрите, буквально вчера я написал 13 строчек, которые строго типизированно загружают все страницы списка задач проекта с гитхаба: https://github.com/eigenmethod/mol/blob/master/app/issues/issues.view.ts#L56
И как это выглядит в действии: https://mol.js.org/app/issues/#projects=reduxjs%2Fredux


А ваших котиков можно было бы загрузить вообще в 3 строки:


cats() {
    return $mol_http.resource( 'cats.json' ).json() as string[]
}

Именно загрузить. Асинхронно. С показом индикатора ожидания и текста ошибки вместо списка котиков. А вы всё обёртки вокруг Redux (и NGRX!) делаете.

Дык оно и в Реакте можно без Redux. Также в одну строчку, используя браузерный fetch API. Как по мне, так основная проблема mol — это его популярность, точнее ее отсутствие. Когда я выбираю фреймворк/библиотеку для нового проекта, одним из важных аргументов является популярность. Просто потому что бизнес может захотеть расширить проект, привести новых людей в команду. При использовании популярных решений сделать это будет в разы проще.

Дык оно и в Реакте можно без Redux.

Можно, конечно, при чём тут Реакт?


Также в одну строчку, используя браузерный fetch API.

Этак разве что без индикатора загрузки. В Реакте можно через SuspenseAPI достигнуть одной строчки.


основная проблема mol

Да не в том суть, а в том, что с Редаксом кода получается вагон, архитектура вывернутая кишками наружу. Зато таймтревел. Будто иначе его не реализовать.


привести новых людей в команду. При использовании популярных решений сделать это будет в разы проще

Если бы. Приходит Реактовод на Ангулярный проект. Сколько потребуется на его переобучение? Да даже если Реакт. Одни с Редаксом работали, другие без — это опять же небо и земля. Толковых разработчиков и так дефицит, так что отсеивать по опыту работы с фреймворком — контрпродуктивно.

16 строчек бойлерплейта, которые опять же ничерта не делают.

Я бы еще заметил, что это 16 строчек бойлерплейта, лишенные того, что автор счёл «общим кодом» (ActionStandard и далее по списку). Это, мягко говоря, нечестное сравнение.

ЗЫ: Вот как раз из-за бойлерплейта не хочу браться за Redux вообще. И авторская статья тут не только не убеждает в обратном, но даже и наоборот. Когда у нас некие несложные но многочисленные типовые интерфейсы и квадратно-гнездовой подход — Redux будет даже, наверное, приятен: слепил весь бойлерплейт на все типичные случаи и сиди дальше штампуй формочки. В противном случае необходимость сидеть писать структурные нагромождения по любому, в том числе и любому очень банальному поводу — вымораживает.

Это верно подмечено. Сделано было для пущего драматического эффекта. Но на самом деле ActionStandard вынесен в библиотеку, createReducer вынесен в библиотеку, так что их и не придется писать. Написать надо будет разве что reducerLoadingMap. Но пара лишних строчек на reducerLoadingMap с лихвой окупится, когда будет работа не с одной сущностью, а с десятью.

Я бы даже сказал, что не из-за бойлерплейта как такового. А из-за того, что в каждом конкретном проекте все эти проблемы решаются по-разному с помощью десятка библиотек и велосипедов разной степени свежести, популярности и упоротости.


И вот вроде все тот же React + Redux. А вникать приходится каждый раз заново. Часто думаешь — пусть уж лучше будет бойлерплейт.

Это, по-моему, в принципе, самая большая проблема экосистемы реакта — отсутствие стандартов, которые бы поддерживались профессионалами на зарплате, а не энтузиастами.
Поэтому лично мне хочется на данном этапе иметь набор библиотек, которые помогут побороть бойлерплейт, но при этом будут оставаться максимально простыми, с как можно меньшим количеством проприетарного API.
Это, по-моему, в принципе, самая большая проблема экосистемы реакта — отсутствие стандартов, которые бы поддерживались профессионалами на зарплате, а не энтузиастами.

Скажем так, поддержка стандартов людьми на зарплате — вовсе не гарантирует того, что итог будет хорошим. Мы все видели ангуляр, в конце концов.
Итог обычно хорош как раз тогда, когда вот вселенский разброд и шатание в конце концов порождает понимание, как делать стоит, а как — не стоит. Боль от необходимости поддерживать то, что было написано на кривых старых либах — это не отменит, но зато будущее будет скорее светлым, чем наоборот.

Так что будем надеяться, что с течением времени хорошие практики тут постепенно сложатся. Не обязательно для конкретно реакта кстати, реакт хорош, но не безпроблемен, и весь мир на нём не сошелся.

Пойдем еще дальше:


@connectResources((state, props) => ({
  users: usersResource.list(state, { query: props.query }),
})
class UsersList extends React.Component {
  propTypes: {
    users: PropTypes.shape({
      fetching: PropTypes.boolean.isRequired,
      failed: PropTypes.boolean.isRequired,
      data: PropTypes.array,
    })
  }
}

Все это разворачивается в классический бойлерплейт. Данные просто подключаются к компоненту: если они уже есть в Redux store, то приходят оттуда, если нет – то сначала загружаются в store из сети, а потом прокидываются в компонент. По умолчанию данные всегда обновляются из сети на componentDidMount.

Не только можно, но даже нужно! Единственное, я для себя пришел к выводу (на данный момент), что подобные обертки надо писать каждый раз заново для каждого проекта. Все проекты немного разные с разными требованиями, поэтому я пока предпочитаю иметь в наличии множество небольших кубиков (таких как flux-action-class, набор стандартных редьюсеров для работы со списками и т.д.) и собирать из них конечный велосипед под нужды проекта. В конечном итоге, да, стремлюсь собрать заниженный велик с турбированным движком и тонировкой по периметру, чтобы получалось что-то похожее на то, что вы предложили.

Главное не пытаться запилить абстрактно, а вытачивать в бою.

Могу быть неправ. Но, reselect — только и нужен для мемоизации сложных преобразований, если она не нужна — можно писать свои селекторы с О(1) в виде функций. Итог: понимай зачем нужны инструменты которые используешь.

А вобще, посмотрите пожалуйста на библиотеку redux-act. Она позволяет строить экшены без голых {type: "...."} конструкций и разбивать редьюсер на красивые функции-обработчики с ресолвом через обьект вместо switch. Почти все что вы описали в статье уже реализовано.

Мысль как раз в том и была, что не стоит бездумно использовать все, что под руку попадется. Пример с селекторами привел лишь потому, что в документации к самому NGRX предлагается использовать мемоизацию на каждый чих.


Redux-act — да, клевая штука. Разве что их createAction с TypeScript дружит не так хорошо, как flux-action-class, а createReducer немного перегружен на мой взгляд по сравнению с redux-create-reducer, потому что парень хотел ES5 поддержать

Вы просто не умеете готовить redux-act :)


import { createReducer, createAction } from 'redux-act'
import axios from 'axios'
import { API } from 'src/constants'

const NS = 'POST__'

const initialState = {
  item: {},
  isLoading: false,
}

export type IState = typeof initialState

const reducer = createReducer<IState>({}, initialState)

const readItemRequest = createAction(`${NS}READ_ITEM_REQUEST`)
reducer.on(readItemRequest, (state) => ({
  ...state,
  isLoading: true,
}))

const readItemSuccess = createAction<IState['item']>(`${NS}READ_ITEM_SUCCESS`)
reducer.on(readItemSuccess, (state, item) => ({
  ...state,
  item,
  isLoading: false,
}))

const readItemFailure = createAction(`${NS}READ_ITEM_FAILURE`)
reducer.on(readItemFailure, (state) => ({
  ...state,
  isLoading: false,
}))

export const readPost = (id: number): Store.IReduxThunkCallBack => (dispatch) => {
  dispatch(readItemRequest())
  return axios
    .get(`${API}posts/${id}/`)
    .then(({ data: item }) => {
      dispatch(readItemSuccess(item))
    })
    .catch((error) => {
      dispatch(readItemFailure())
      return Promise.reject(error)
    })
}

export default reducer

А общая обработка ошибок построена на соглашении, что все функции redux-thunk возвращают Promise:


export const dispatch = (action) => {
  if (isPromise(action)) {
    return store.dispatch(action).catch((error) => {
      if (error) {
        console.error(error)
      }
      return Promise.reject(error)
    })
  }
  return store.dispatch(action)
}

Компоненты должны дергать только эту обертку для store.dispatch() (вместо dispatch(), который приходит параметром в mapDispatchToProps() получаемого из connect()).


Конечно, большой соблазн обобщить шаблонный код: _REQUEST > вызов axios > _SUCCESS > _FAILURE, видел несколько реализаций, но теряется читабельность, на мой вкус.


Я сейчас в процессе очередной попытки получить "аленький цветочек", реализую шаблон проекта на стеке Create React App + Type Script + Antd Design Components + Styled Components.

Sign up to leave a comment.

Articles