Как стать автором
Обновить

Комментарии 11

Я может не до конца понял суть (наверное), но вы, кажется, избавились вовсе не от reducer-ов, а скорее сократили цепочку actionCreator -> action -> reducer -> view, до something -> view. Т.е. впервую очередь выпилили сами action-ы. Сделали там монолит. Вы не первый ;) Где something это всё-в-одном. Нечто подобное я сегодня читал в документации в vuex.


Касательно тех 8 пунктов-претензий. Я думаю мы все сталкивались с ними, но каждый с ними сражался как-то по своему.


Вот, скажем, вам не нравится, что в базовом виде, action пробегает через множество switch-case-ов. Точнее через все. Это позволяет менять на 1 action store сразу в нескольких местах. Мне такая возможность сразу показалась проблемной, и я от неё отказался без задней мысли. У меня в reducer-ах нет никаких switch-ей. Прямое сопоставление action.type к map-е, которая его обрабатывает. 1 action = 1 handler. Никаких проблем с "путаницей" что-куда-где у меня не было. Там всё предельно просто и железно, т.к. у action-ов есть свои префиксы, исходя из вложенности.


Или вот вы пишете, что есть большие проблемы с доступом к той части state, которая напрямую не касается текущего вложенного reducer-а, но нужна для правильной работы логики в нём (read-only естессно). У меня нет таких проблем, т.к. мои вложенные reducer-ы имеют такую сигнатуру, которая мне нужна в этом месте. Т.е. любую. И обычно rootState в ней идёт 3-им аргументом. Да и вообще я выстраиваю reducer-ы по ситуации. К примеру могу весьма специфически обрабатывать их результат (а не просто вернуть его же как значение поля). Очень помогает в рамках IoC.


Так же вы писали, что вам не нравится, что доступ к rootState-у в mapStateToProps захламляет код. Ну я просто использую custom-ый connect-метод, который это учитывает. Долго объяснять механику. Но суть в том, что вообще никто не заставляет вас использовать обычный connect. У вас есть subscribe метод, и вы можете реализовать хоть иерархический, хоть сколь угодно другой причудливый HoC. Скажем как бы вы реализовали что-то вроде excel-я? Надо будет включить мозг и писать своё решение.


Вы писали про груды кода в actionCreator-ах. Тут у меня ключевое отличие. У меня в них практически никогда нет никакого кода. Только если речь касается какой-то специфики (типа асинхронных api-запросов). Да и там я стараюсь писать actionCreator-ы предельно тупыми. Мне кажется это последнее место для реализации серьёзной бизнес-логики. По сути все сложные (не-DOM) вещи у меня собраны в 1) reducer-ах, 2) в selector-ах.


Затем вы пишете, что очень мешает то, что каждый action завязан на множество файлов. В одном reducer, в другом константа, в третьем только import, в четвёртом ещё бог знает что. Я довольно быстро пришёл к схеме, когда у каждого под-модуля есть свой файл redux.js, где сразу и reducer (по сути просто map-object где ключи = action.type), и actionCreator-ы, и selector-ы (если надо). Это позволяет избавиться от львиной доли мусорных экспортов и импортов.


Небольшой пример такого очередного redux.js:


const A = actionFactory('PREFIX'); // make a fabric

export const aDel = A.create('DEL', 'id');
export const aClose = A.create('CLOSE');
export const map =
{
  [aDel](st, { id }){ /*...*/ },
  [aClose](st){ /* ... */ },
};
Помимо желания поделиться своим опытом (о чем многим, уверен, было бы интересно почитать подробнее), в вашем комменте читается желание покритиковать. Но вот только я пока не понял что именно))
Создатели Redux не принуждают зацикливаться на дефолтном combineReducers, чем пользуетесь и Вы и я.

Позвольте только вопрос. Насколько понимаю, action-type у вас является константа вида «A.B.C» (либо массив/объект). Вы его импортируете из того же файла где лежит и хэндлер. А потом, диспатчем бросаете тип в «черный ящик», который, по факту, тут же вызывает ваш хендлер по принципу reducer['A']['B']['C'](state, action). В чем сакральный смысл такого непрямого вызова? Активно пользуетесь мидлварами? Или просто удобно часть «модульной» логики класть в редьюсеры вне хэндлеров?

Про эксель — решал бы в лоб.
Рендерятся те ячейки, что помещаются в видимую область. Каждая ячейка имеет адрес и данные в стейте в Map-е data[address]. С помощью immutable-selectors удобно получать данные из коллеккций. Объект — дерево выглядел бы таким образом:
const selectors  = {
  data:{
    param:'x'
  }
};

Ячейки привязаны стандартным connect через mstp вида
return{
  value:selectors.data(state, ownProps.address)
}


По изменению ячейки, либо блуру, вызов action-creator-а
export function onChange(address, newValue) {
  return function (dispatch, getState) {
    dispatch({
      type:'Изменение ячейки',
      setState: selectors.data.replace(getState(), newValue, address),
      payload: { address, newValue }
    });
  };
}


Судя по имеющемуся опыту, каких либо проблем быстродействия тут быть не может. Даже если добавить вычисление эпических формул между ячейками.
Позвольте только вопрос. Насколько понимаю, action-type у вас является константа вида «A.B.C» (либо массив/объект). Вы его импортируете из того же файла где лежит и хэндлер. А потом, диспатчем бросаете тип в «черный ящик», который, по факту, тут же вызывает ваш хендлер по принципу reducer['A']['B']['C'](state, action). В чем сакральный смысл такого непрямого вызова? Активно пользуетесь мидлварами? Или просто удобно часть «модульной» логики класть в редьюсеры вне хэндлеров?

Честно говоря, я вас не понял. Но попробую ответить:


  1. action-type-ом у меня являются строки, в которых есть префиксы
  2. action-type-ы никуда не импортируются (а зачем?)
  3. вместо них импортируются actionCreator-ы для mapDispatchToProps
  4. "reducer"-ы не вызываются по принципу reducer[prefix1][prefix2][prefix3]. reducer-ы не являются какими-то очень универсальными, работающими по какому-то конкретному механизму. Они могут быть произвольными. Ну вот так например:

import { map as mod1 } from 'mod1/redux';

// reducer in the middle
export default (state, action, rootState) =>
{
  if(action.type.startsWith('mod1'))
  // or if(action.type in mod1)
  {
    const handler = mod1[action.type];
    const field1 = handler(state.field1, action, rootState);
    return { ...state, field1 };
  }

  if(action.type in ownMap)
    return ownMap[action.type](state, action, rootState);

  throw new Error(`unsupported action-type: ${action.type}`);
}

По сути ― тут идёт проверка на action-type и перенаправление его вложенному reducer-у. Предполагается, что вложенный reducer работает с каким-то конкретным полем. Вызываем его с нужным ему куском state-а и по результату меняем его же поле в state.


Пример типовой, по факту же всё может быть совсем по-другому, т.к. real life задачи бывают сильно сложнее и причудливее. Но логика та же — в вышестоящем reducer-е мы можем произвольным образом вызывать нижестоящие. Если там удобно написать что-то хитрое — пишем. Если там всё просто — стараемся сделать всё предельно декларативно, почти без кода.


диспатчем бросаете тип в «черный ящик»

Понравилась формулировка. Я так понимаю, этим вы хотели сказать, что нижестоящий reducer не знает как с ним будет работать вышестоящий reducer и будет ли вообще. Да? Ну дак это прекрасно, имхо. IoC в действии.


Активно пользуетесь мидлварами?

Только thunk, не более. Схема простая как валенок, middlewares не требуются.


Или просто удобно часть «модульной» логики класть в редьюсеры вне хэндлеров?

Тут пожалуй я окончательно запутался. Что есть "модульная логика" и что есть "хэндлер"? В reducer-ах у меня есть "кастомная" логика по распределению ответственности — какой конкретно подредсьсер будет ответственным за какие типы action-ов, и какие куски state-а его должны интересовать, и как их обрабатывать. То как это сделать в конкретном месте — решается согласно задаче. По сути так, как это будет удобнее. Чем меньше нижестоящий reducer знает лишнего, тем лучше, не его ума дело. Поэтому сигнатуры от задачи к задаче могут меняться. Ладно, что-то меня понесло в дебри.


Ячейки привязаны стандартным connect через mstp вида

Ну вот тут мы и приехали. Сразу. Судите сами: у вас на экране 20 колонок и 30 строк. Это 600 ячеек. Сразу 600 subscribe-ов к connect-у. Которые будут отрабатываться вообще на любой чих. Какую часть store вы бы не изменили, вызываются ВСЕ АБСОЛЮТНО подписчики. Берём экран побольше — теперь у нас не 20х30, а скажем 25х40 = 1000. Беда. Усложняем ячейки (они же по своей природе могут быть сложносоставными). Получаем 2000, 3000, 4000… И т.д… Так проект не взлетит.


В чём проблема? В том что у вас вызываются тысячи mapStateToProps. Они что-то делают (обычно они легковесные). Их результат подвергается shallow-comparison проверке. Чем больше полей, тем дольше. Практически всегда проверка выдаёт, что данные те же, и дальнейший render не требуется. Приемлемо ли это? Нет, конечно. Есть ли нормальные пути решения? Да, конечно.


Как решать? Ну первое что приходит в голову, это иерархический connect. Скажем если поменялось что-то вне таблицы, то зачем вызывать эти наши тысячи mapStateToProps? Стало быть subscribe на таблицу должен быть 1, а в нём уже кастомная логика дилегирования нижестоящим звеньям. Ну дальше надо уже плотно думать опираясь на задачу. По сути говоря задача написать такой "connect" который опираясь на кастомную логику задачи будет дёргать как можно меньше лишних участков кода. Из коробки redux даёт доступ к глобальной области всем connect-ам. Ввиду этого вынужден вызывать вообще все subscribers.


Судя по имеющемуся опыту, каких либо проблем быстродействия тут быть не может. Даже если добавить вычисление эпических формул между ячейками.

Я надеюсь это была шутка )

Получаем 2000, 3000, 4000… И т.д…

Я однажды видел реализованную в экселе картографию. Реально, карта России, нарисованная пиксельно в ячейках. Внутри карты так же пиксельно раскрашивались регионы в соответствии с показателями. Среди всей гаммы чувств, при виде этого, особенно сильными были «Офигеть!» и «Нафига?». Вы, видимо, собираетесь не меньше чем сапера замутить)))

Если серьезно, то вот я взял первый попавшийся реальный файл с постановкой. На фулскрине — 12*16 ячеек. Я уверен, что даже 400 привязанных ячеек(по одному значению) не вызовут заметных лагов на блур. Другой вопрос, что это, как Вы заметили, «архитектурно не оптимально». Однако, пока это дешево в реализации, решает задачу и не лагает, это может быть приемлемо.

Connect со своей логикой — это круто. Надо будет поковыряться на досуге в этом направлении. Может быть есть где почитать кроме исходников react-redux?

Еще вариант оптимизации на стандартном connect — это сделать составной ключ строка*столбец, аналогично хранить данные data[row][col]. Привязать только таблицу, сделать отдельным компонентом строки. Строки и ячейки как PureComponent. При рендере, компонентам раздавать свои данные. Количество shallow-comparison проверок будет много меньше.
Однако, пока это дешево в реализации, решает задачу и не лагает, это может быть приемлемо.

Ну тут судите сами. 12*16 это мелочь. Это не файл. Скажем когда мы делали на knockout аналог объектного-word-а, нам нужно было обрабатывать документы >1500 стр… В них были таблицы на сотни страниц. В них были объединённые ячейки на тысячи строк. В общем был масштаб. Такого же рода может быть и excel-документ гос-го толка. Отсюда простейший вывод, если вы правда делаете excel, то без виртуального скроллинга вы никуда не уедете. А ещё вам придётся учесть, что уж больно много элементов на экране, к тому же они могут быть весьма сложно устроены. Прямая реализация в лоб невозможна. Надо будет разбираться в используемых технологиях, алгоритмах, структурах данных, к самим подходам к таким вещам и пр. и пр… В частности никаких стандартных mapStateToProps в тысячах или десятках тысяч instance-ов там быть не может. Это же чудовищная bigO.


А теперь вернёмся к примеру про 12*16 и примитив. У вас на любой чих вызывается минимум 192 mstp. На это уходит батарейка. И ресурсы. Вспомните про старые андроид трубки. В общем так мыслить нельзя. Если у вас 99.999% работы проходит в пустую, то надо менять подходы. Желательно до реализации, а не после.


Потом на нас жалуются, что на наших десктопных машинах по 32 GiB памяти, SSD и пр. и пр., и у нас "не тормозит", поэтому мы пишем тяп-ляп. А у клиентов всё бывает очень грустно. В дев-тулзах даже есть специальные замедлители.


Может быть есть где почитать кроме исходников react-redux?

Не знаю. Я в эту сторону пока не копал, задачи такой не было, но я в целом избегаю connect-ов в тех местах, где у меня есть большие списки или большие деревья, предпочитая там просто props с компонентов выше. Т.к. не вижу смысла наступать на грабли.

Наверное у Вас интересный подход. Но не оень пока понятно в чем именно онзаключается. Я бы предложил Вам убрать в статье все ссылки на «классический» redux т.к. его достоинства и недостатки известны достаточно хорошо.
Я не так много писал на redux и для себя сделал некотороые выводы по поводу того как это можно немного упростить.
1. Я не пропускаю весь ввод с клавиатуры в формах через redux. Т.к. по сути изменение сосотояния приложения произойдет после отправки данных на сервер. (Использую state)
2. Я совместил определение констант экшшинов и редьюсера в одном файле (пдосмотрел у github.com/erikras/react-redux-universal-hot-example)

С моей точки зрения вопрос с redux в ближайшее время перейдет в немного другую плоскость. Т.к. грядет react suspense см. habr.com/post/350816 которое как мне кажется будет сильно конфиликтовать с логикой redux.

Если уже указывать на глобальный недостаток redux я бы назвал в первую очередь его имперавтивный (не декларативный) подход к разраотке приложения. Я больше склоняюсь к широкому использованию связки graphql/apollo см. habr.com/post/358942 которые позволяют декларативно описывать состояние приложения.
Я совместил определение констант экшшинов и редьюсера в одном файле


этот подход называется duck modules
Подход в следующем:
  1. Обрабочтики компонет (экшены) непосредственно мутируют глобальный стейт. Диспатчем просто новая версия кладется в store
  2. Для удобства работы с глобальным глубоко вложенным стейтом, используется библиотека.


Я не пропускаю весь ввод с клавиатуры в формах через redux. Т.к. по сути изменение сосотояния приложения произойдет после отправки данных на сервер. (Использую state)

Это у вас форма ввода слишком простая. От значения вашего контрола ничего не зависит. Даже в статье, это не так.
За ссылки спасибо

Если кратко охарактеризовать сущность вашей схемы в отличии от базовой:


В самой в базовой схеме все actionCreator-ы обязаны создавать plain object action с полем type. В расширенной для асинхронности добавлены redux-thunk или redux-saga или ещё чего. В вашем случае видимо redux-thunk. Вы используете его для получения getState. В базовой схеме reducer выполняется над данными после dispatch-а action-а в store. У вас же наоборот, reducer выполняется в обход redux-а, когда душе угодно, а в redux уже уходят готовые данные для замены. В базовом виде reducer-ы дробятся иерархично, и каждому подредьюсеру доступен свой кусок state-а. У вас всё работаете через getState и линзы-селекторы, т.е. "глобально". Ну и в целом если подвести — в базовом виде action-ы, actionType-ы, actionCreator-ы, reducer-ы — всё отдельные сущности, с друг другом связанные лишь косвенно (к примеру action.type-ом). В этом философия ынтерпрайза на redux. У вас это одна большая сущность, которую при желании можно декомпозировать, но можно и не декомпозировать.


Подходов когда выбрасывается большая часть redux цепи уже много. Ваш от большинства тех, что я видел отличается тем, что getState вызывается до dispatch-а plain action-а. Т.е. у вас "внешний" reducer. А потом все action-ы thunk-уты. Обычно делали не так, то что у вас называется setState и является скорее newState: data, обычно это делают методом, который вызывается всё таки из reducer-а, а не за его пределами. Хотя результат тот же.


Все эти подходы напоминают Vuex. Там посмотрели на redux, посмотрели на vue, взяли redux и написали с нуля, но выкинули практически всю ынтерпрайзную бюрократию, добавили какой-то своей мути (модули), добавили к этой мути костылей, получили довольно компактное решение. В живую ещё не пробовал, но после пары лет redux-а — подход Vuex выглядит гораздо аппетитнее. Особенно ввиду отсутствия необходимости морочить себе голову с immutable-значениями, там вместо этого observable-реактивность. Планирую следующий небольшой проект сделать на vue+vuex чтобы распробовать.

Полистал сейчас vuex доку пару минут. Получается ридьюсеры у них называются mutations, при этом мутировать можно напрямую. Полагаю в мутатор функцию аргументом приходит прокси стейта, поэтому можно и напрямую. Получается у них там mobx-like стор с мутаторами (actions в случае mobx). Например с immer тоже можно мутировать стейт напрямую (библиотеку запилил тот же чувак что и mobx, полагаю это и есть кусок функционала из mobx), только там есть разные особенности — с некоторыми видами стейта он не работает (просто висит бесконечно), например с перелинкованными между собой данными. PS и actions в vuex тоже есть, сразу не заметил.

Я не вижу особенной громоздкости в redux подходе. Если экшены объявлять компактным образом используя github.com/pelotom/unionize — like библиотеку и редьюсеры писать с immer/hydux-mutator подобной библиотекой хелперов иммутабельного патчинга стейта. Зато меньше «магии» чем с mobx подобными штуковинами.
Полагаю в мутатор функцию аргументом приходит прокси стейта, поэтому можно и напрямую.

Там (Vue) под капотом observer. Это совсем другой подход к реактивности. Наиболее классический. Никакой иммутабельности, shallow comparison и пр. Мутировать можно что угодно, где угодно и когда угодно, и всё будет работать. Vuex позволяет это всё как-то систематизировать в духе Flux/Redux. Своего рода добровольные кандалы во имя порядка. Насколько Vuex популярно в мире Vue — не знаю. Mobx не пробовал пока, за него не скажу ничего (но людям нравится).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории