В данном посте мы коснемся написания action'ов и reducer'а. Для начала рассмотрим типичный 'flow', в котором мы выполняем следующие операции (далее переработаем все так, чтобы наш код отвечал принципам SOLID).

1. создаем файл с константами (здесь мы сохраняем названия типов action'ов)

export const REQUEST_DATA_PENDING = "REQUEST_DATA_PENDING";
export const REQUEST_DATA_SUCCESS = "REQUEST_DATA_SUCCESS";
export const REQUEST_DATA_FAILED = "REQUEST_DATA_FAILED";
export const PROFILES_PER_PAGE = "PROFILES_PER_PAGE";
export const CURRENT_PAGE = "CURRENT_PAGE";

2. создаем файл, где описываем action'ы (здесь мы делаем запрос на получение учеток пользователей, и пагинация). Также в примере был использован redux-thunk (далее мы откажемся от подобных зависимостей):

export const requestBigDataAction = () => (dispatch) => {
    fetchingData(dispatch, BIG_DATA_URL, 50);
}

export const changeCurrentPAGE = (page) => ({
    type: CURRENT_PAGE,
    payload: page
})

function fetchingData(dispatch, url, profilesPerPage) {
    dispatch({type: REQUEST_DATA_PENDING});
    fetch(url)
        .then((res) => {
            if(res.status !== 200) {
                throw new Error (res.status);
            }
            else { 
                return res.json();
            }
        })
        .then((data) => {dispatch({type: REQUEST_DATA_SUCCESS, payload: data})})
        .then(() => dispatch({type: PROFILES_PER_PAGE, payload: profilesPerPage}))
        .catch((err) => dispatch({type: REQUEST_DATA_FAILED, payload: `Произошла ошибка. ${err.message}`}));
}

3. мы пишем reducer

import { REQUEST_DATA_PENDING, REQUEST_DATA_SUCCESS, REQUEST_DATA_FAILED, PROFILES_PER_PAGE, CURRENT_PAGE } from '../constants/constants';

const initialState = {
    isPending: false,
    buffer: [],
    data: [],
    error: "",
    page: 0,
    profilesPerPage: 0,
    detailedProfile: {}
}

export const MainReducer = (state = initialState, action = {}) => {
    switch(action.type) {
        case REQUEST_DATA_PENDING:
            return Object.assign({}, state, {isPending: true});
        case REQUEST_DATA_SUCCESS:
            return Object.assign({}, state, {page : 0, isPending: false, data: action.payload, error: "", detailedProfile: {}, buffer: action.payload});
        case REQUEST_DATA_FAILED:
            return Object.assign({}, initialState, {error: action.payload});
        case PROFILES_PER_PAGE:
            return Object.assign({}, state, {profilesPerPage: action.payload});
        case CURRENT_PAGE:
            return Object.assign({}, state, {page: action.payload});
        default:
            return state;
    }
}

4. настраиваем store (применяем middleware thunkMiddleware)

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import {MainReducer} from './reducers/mainReducer';

const store = createStore(MainReducer, applyMiddleware(thunkMiddleware));

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


5. подключаем компонент к redux
const mapDispatchToProps = (dispatch)=>{
    return {
      onRequestBigData: (event) =>{
          dispatch(requestBigDataAction());
    }
    }
};

подключаем кнопки пагинации к redux

const mapDispatchToProps = (dispatch)=>{
    return {
      onChangePage: (page) =>{
          dispatch(changeCurrentPAGE(page));
        }
    }
};

Проблема: наш редьюсер представляет собой одну большую инструкцию switch, следовательно при добавлении нового action'а, или изменения его поведения нам необходимо изменять наш редьюсер, что нарушает принципы SOlid (принцип единственной ответственности и принцип открытости/закрытости).

Решение: нам поможет полиморфизм. Добавим к каждому action'у метод execute, который будет применять обновление и возвращать обновленный state. Тогда наш reducer примет вид

export const MainReducer = (state = initialState, action) => {
    if(typeof action.execute === 'function') return action.execute(state);
    return state;
};

теперь при добавлении нового action'а нам не понадобиться изменять reducer, и он не превратиться в огромного монстра.

Далее откажемся от redux-thunk и перепишем action'ы

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
// import thunkMiddleware from 'redux-thunk';
import {MainReducer} from './reducers/mainReducer';

const store = createStore(MainReducer);

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

переходим к подключенному компоненту, action которого асинхронный (его придется совсем слегка подкорректировать)

const mapDispatchToProps = (dispatch)=>{
    return {
      onRequestBigData: (event) =>{
          requestBigDataAction(dispatch);
    },
    }
};

и перейдем к самим action'ам и добавим им метод execute

const type = 'bla-bla';

const requestDataPending = {execute: state => ({...state, isPending: true}), type};

const requestDataSuccess = payload => ({
    execute: function (state) {
         return ({...state, 
            page : 0, 
            isPending: false, 
            data: payload, 
            error: "", 
            detailedProfile: {}, 
            buffer: payload})
        }, 
            type})

const profilesPerPageAction = profilesPerPage => ({
    execute: state => ({...state, profilesPerPage: profilesPerPage}),
    type
});

const requestDataFailed = errMsg => state => ({...state, error: `Произошла ошибка. ${errMsg}`});

function fetchingData(dispatch, url, profilesPerPage) {
    dispatch(requestDataPending);
    fetch(url)
        .then((res) => {
            if(res.status !== 200) {
                throw new Error (res.status);
            }
            else { 
                return res.json();
            }
        })
        .then((data) => {dispatch(requestDataSuccess(data))})
        .then(() => dispatch(profilesPerPageAction(profilesPerPage)))
        .catch((err) => dispatch(requestDataFailed(err.message)));
}

export const requestBigDataAction = (dispatch) => {
    fetchingData(dispatch, BIG_DATA_URL, 50);
}

export const changeCurrentPAGE = page => ({
    type,
    execute: state => ({...state, page})
})

Внимание: свойство type обязательное (если его не добавить, будет выброшено исключение). Но для нас оно не имеет вообще никакого значения. Именно поэтому у нас отпадает потребность в отдельном файле с перечислением типов action'ов.

P.S.: В данной статье мы применили принципы SRP и OCP, полиморфизм, отказались от сторонней библиотеки и сделали наш код более чистым и поддерживаемым.

Комментарии (27)


  1. Marat1403 Автор
    10.11.2019 13:31
    +1

    Нам вообще не нужен тип экшена, каждый экшен имеет свой метод execute, и вместо того чтобы проверять его тип и в зависимости от типа выполнять преобразование, мы просто вызываем метод execute, а экшен сам знает что делать (то есть мы отходим от процедурного программирования к объектно-ориентированному


    1. funca
      10.11.2019 13:42

      Экшен должен быть простой структурой данных, которую можно сериализовывать. Собственно из-за этого в редаксе столько церемоний. Объект с методами таким свойством не обладает.
      Для композиции редьюсеров "как в солид" можно пользоваться трансдьюсерами, например подключив recompose. Главное понимать, зачем это вам нужно.


      1. JustDont
        10.11.2019 14:02

        Экшен должен быть простой структурой данных, которую можно сериализовывать. Собственно из-за этого в редаксе столько церемоний. Объект с методами таким свойством не обладает.

        Скажем так, организовать трансляцию чего-то сериализующегося в объекты и обратно — это не rocket science. Всю дорогу так делали, начиная с древнейших времен.
        Тут скорее вопрос в том, что нам важнее — чистая сериализация и постоянные ужимки в архитектуре кода (потому что мы теперь пляшем вокруг потребностей сериализации даже в тех областях, которые сами к сериализации никаким боком), или же сериализация у нас отдельным слоем логики со своей собственной магией, но зато остальной код совсем не парится этими проблемами.

        Ну а статья — она вообще, на мой взгляд, очень такая себе. Редакс это opinionated библиотека, что означает, что в принципе оно всё работает, если делать строго так, как задумано авторами. Делать по-другому? А зачем, когда можно просто не пользоваться редаксом? Автор выкинул редьюсеры (логичный шаг с т.з. упрощения архитектуры), и с этим потерял все фичи редакса, которые на этом основаны — ну и зачем тогда редакс вообще?


      1. faiwer
        12.11.2019 00:23

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

        Не должен. Здесь нет догм. POJO это типовое решение. Но не единственное. Но оно, к примеру, позволяет обмениваться action-ми между клиентом и сервером.


        В redux вообще мало этих самых "должен". Там просто есть набор рекомендаций, идеология, и типовые примеры. Вы не обязаны использовать switch-case, вы не обязаны использовать action-constant-type-ы, вы не обязаны использовать combine-reducers (по сути вы можете слепить какой-угодно редьюсер, с какой угодно схемой). Вы даже не обязаны использовать connect. Вам просто дана шина данных и набор советов как ею можно воспользоваться. Не более.


  1. chemaxa
    10.11.2019 13:46
    +1

    Если я правильно понял, то мы таким образом по пути потеряли одну из фичей редакса под названием таймтревел дебагинг?
    И ещё мы теперь не знаем какие экшены и кто и когда вызвал и что они наделали


    1. faiwer
      12.11.2019 00:27

      И ещё мы теперь не знаем какие экшены и кто и когда вызвал и что они наделали

      справедливости ради вы и раньше не всё знали, если использовали что-нибудь вроде redux-thunk, -saga, mstp с dispatch, просто dispatch и т.д.


      А TimeTravel вы не потеряете если будете отталкиваться от срезов состояний, а не от последовательностей action-ов.


  1. kurt_live
    10.11.2019 14:17
    +1

    Так, а как же разбиение модели приложения на несколько редьюсеров? В боевых приложениях обычно бывает композиция из пары десятков редьюсеров — и то, это не самые большие приложения. Тип тут нужен как индикатор будет ли изменён стейт соответствующего редьюсер. Один тип может подействовать как на 1 редьюсер, так и на 3 из 5, например. Вы убили наглядное изменение стейта, убили масштабируемость, да и солид у вас теперь не солид. Экшен знает как менять стор, знает его структуру, а редьюсер знает только execute. Сам смысл понятия редьюсер потерялся. Таким образом можно отказаться от редакса и реакт-редакса в пользу самописного варианта из одного объекта и пары функций — с учётом наличия нового контекст апи — раз плюнуть.

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

    Я работаю с редаксом 4 года и могу сказать точно:
    1. Вы искалечили библиотеку
    2. Вы нарушили сам принцип сингл респонсабилити
    3. Ваш подход привёл к усложнению разработки. Надо бы померять ещё и производительность этого чуда.


    1. Marat1403 Автор
      10.11.2019 16:14

      По поводу комбинации нескольких редьюсеров. В данном примере нет необходимости вообще комбинировать редьюсеры (так как причины для комбинации больше нет, редьюсер не будет разрастаться инструкциями типа switch case). В любом случае вы работатете с одним стейтом, просто при комбинации появляется поле, которое содержит подстейт (вы можете в самом экшене деструктурировать это поле и обработать нужный подстейт). Никаких редьюсеров дополнительно писать не придется.

      Что касается принципа SRP, здесь я с вами не согласен. SRP гласит что должна быть только одна причина для изменения (при использовании инструкции switch количество потенциальных причин для изменения редьюсера = количеству экшенов). Для проверки выполнения этого принципа предлагаю простой прием — если возможна ситуация при которой возможны конфликты (например вы правили один экшен, другой разработчик правил другой и добавил еще один экшен) — скорее всего SRP не был соблюден.


      1. kurt_live
        10.11.2019 22:49
        +1

        Мой предыдущий ответ был съеден хабром, он был остроумен и я старался как мог. но увы, воспроизвести это великолепие я не могу.


        краткие тезисы


        1. Редакс — это фп библиотека, переопределяя редьюсер как функцию с сайдэфектом(дергает некий колбек), вы нарушаете чистоту функции редьюсера. если раньше она отвечала за S' = f(S, A) то теперь она просто обертка вокруг вашего кода. сам редакс построен на композиции чистых функций и иммутабельности, ваш код это теряет. как теряет и масштабируемость, о чем я писал выше.
        2. Тип экшена болтается теперь неприкаянный, убрать вы его не можете, мама редакс заругает, но и применить тоже. Это уже вопит о том, что вы неправильно обращаетесь с библиотекой. если же убрать тип — не понятно, зачем экшены и диспатч в принципе. Тут стоит вставить эту надоевшую всем диаграмму реакт/редакс дата флоу, но я не буду. Без диспатча и типа экшена получается эдакое окно в Европу — ворочу, что хочу. Зачем тогда сложный редакс с его диспатчем, мидлварами и прочим — мне не ясно. Можно написать сервис в пяток строк который будет жрать функции и применять их на некий стейт. Минус еще 2 зависимости, а?
        3. Экшен, который был просто данными(если утрировать, то это адрес шины данных и данные для этой шины), превратился в супер качка и грозит навалять очкарикам ударной левой, что его чмырили за отсутствие функционала. он уже отжал у них не только учебники, но и карманные деньги на месяц
        4. Вы удалили мидлвар в 13 строк и раздали ее обязанности непричастным. Думаю, redux-thunk многих смущает своим всемогуществом. Однако, дело редаксовых мидлвар — преобразовывать экшены или их предтечи, делая из сложного(например функции с сайдэффектом, как это делает thunk, или из промиса или генератора) простое — набор данных(экшенов). У вас преобразованием занялся редьюсер, а работой редьюсера занялся экшен. и тот и другой не подходят для этого. При этом, механизм мидлвар никуда не делся. То как вы трактуете солид и его применение — мягко говоря, не канон. Я привел бы такой пример: представьте маленький офис, в котором трудятся трое — офис менеджер — следит за уютом, наличием интернета, оборудования, кофе и плюшек, проект-менеджер — занимается всей проектной работой, коммуникацией по проекту, срокам, цене, требованиям и т.д. и программист — преобразует кофе, плюшки и требования в код к определенному сроку, отправляя все изменения по интернету в удаленный репозиторий. Каждый при деле, работа кипит. Приходит заказчик, который раньше присылал только требования, и увольняет программиста с целью оптимизации, менеджерам же он раздает его обязанности со словами: мне кажется программист подворовывал кофе и плюшки, я попросил его уйти по-хорошему. Зарплату вам поднять? не-не. Такая вот остросоциальная драма в коде.
        5. Инструменты преследуют своей формой какую-то цель, а не принцип. да, они используют принципы, но чтобы отсечь сложность и дать определенный функционал, а не пишутся, чтобы использовать некий принцип. Уверен, авторы в курсе про солид, грасп, ягни, кисс, драй и прочих зверушек и читали Мартина. Но они выбрали иные принципы, потому что они делают универсальный и надежный инструмент, который позволит делать еще более сложные и увлекательные вещи. В конце концов, на те же принципы они могут иметь свой взгляд отличный от вашего.
        6. Редакс и так хорош, особенно если обуздать боллерплейт. Вы можете улучшить девелопер экспириенс, уменьшив количество боллерплейта, при этом не ломая библиотеку через бедро с криком "SOLID!".
        7. Вы вполне можете применить солид в собственном стейт-менеджере, не мучая котиков(в данном случае редакс). Да, хайпануть на редаксе не получится, но получится хайпануть на своем коде и солиде. Больше плюсов, чем минусов.

        Конечно, в съеденном хабром ответе был еще разбор на примере микросхем 16-битного ЭВМ, но такова судьба — вам он не достался.


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


        1. sultan99
          10.11.2019 23:36

          золотые слова :)
          эх жаль что можно поставить один плюс…


        1. xGromMx
          10.11.2019 23:53

          в каком месте это фп?


          1. kurt_live
            11.11.2019 08:25

            В том как он себя ведет и какие принципы использует — это фп. да, это не хаскель, поэтому тру фпшники могут пинать меня сколько угодно.


            однако,


            1. все апи редакс это функции(applyMiddleware, compose, createStore, bindActionCreators, combineReducers), которые принимают другие функции или простые объекты(это допустимо в js и не делает из библиотеки не ФП).
            2. редакс использует чистые функции(reducers)
            3. редакс ожидает иммутабельного состояния
            4. редакс использует карирование
            5. редакс использует композицию функций, а не наследование.
            6. редакс не содержит классы в исходном коде — только плейн объекты и функции. стор это плейн объект.
            7. разработчики редакса сильно не рекомендуют использовать наследование, инстансы классов и прочее ооп при работе с библиотекой — сломается сериализация и таймтревел. https://redux.js.org/faq/design-decisions#why-doesnt-redux-support-using-classes-for-actions-and-reducers как пример
            8. this встречается часто, но на использование в коде — 3 раза, 1 раз, когда нужно вернуть ссылку на обсервабл(он внезапно тоже не инстанс какого-то класса, а просто {}) из него же https://github.com/reduxjs/redux/blob/master/src/createStore.ts#L332, что понятно — у него нет имени, и 2 раза — как имя параметра функции https://github.com/reduxjs/redux/blob/master/src/bindActionCreators.ts#L12. остальное — комментарии и строки с сообщениями об ошибках. это не противоречит фп на жс.

            Если это не фп, то мне очень интересно услышать вашу версию.


          1. VolCh
            11.11.2019 09:51

            Ядро редакса — это чистое ФП., "грязь" начинается там, где хранится история, ну и, как я подозреваю, там где приходится обходить недостатки языка, типа неимплементированной оптимизации хвостовой рекурсия.


        1. mayorovp
          11.11.2019 14:15

          переопределяя редьюсер как функцию с сайдэфектом(дергает некий колбек), вы нарушаете чистоту функции редьюсера

          Ну не всё так плохо: если потребовать от колбека чистоты — то вызов этого колбека также никакой чистоты не нарушит.


          1. kurt_live
            11.11.2019 14:34

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


            1. mayorovp
              11.11.2019 14:42

              А зачем на практике проверять, что колбек не вызовет сайдэффектов? Их надо просто без сайдэффектов писать.


              1. kurt_live
                11.11.2019 17:29

                не правильно выразился, наверно. "гарантировать" :) реакт в данном случае дергает в девелопменте методы жизненного цикла пару раз, чтобы поймать разработчика "за руку", так сказать, но иногда и это не срабатывает.


                1. faiwer
                  12.11.2019 00:32

                  Не стоит на это полагаться, всё же :)


  1. aleki
    10.11.2019 23:35

    На один action могут реагировать несколько reducer’ов, в вашей же реализации это невозможно. Этого уже было достаточно, чтобы не городить этот огород.


    1. Marat1403 Автор
      11.11.2019 00:15
      +1

      В моей реализации в принципе нет нескольких редьюсеров. Каждый экшен получает весь стейт и способен обрабатывать как один так и несколько подстейтов (ведь при комбайне вы получаете объект, каждому полю которого соответствует некий подстейт). Отпадает ситуация, когда бизнес-логика одного экшена размазывается по нескольким редьюсерам.


      1. Ogra
        11.11.2019 05:33

        Обрабатывать несколько подстейтов в одном редьюсере — тоже серьезное нарушение single responsibility
        Вы пытаетесь изолировать ответственность по экшенам, а combineReducers изолирует ответственность по подстейтам.
        Из этой идеи можно запилить свою свою библиотеку, но вот с redux так обращаться не стоит.


      1. VolCh
        11.11.2019 09:46

        Когда слабосвязанные аспекты бизнес-логики одного экшена запихнуты в один редьюсер как раз и нарушается SRP.


    1. faiwer
      12.11.2019 00:34

      На один action могут реагировать несколько reducer’ов, в вашей же реализации это невозможно

      Это единственное что мне понравилось в статье. Исповедую этот подход с самого начала работы с redux. На мой взгляд эта возможность сильно избыточна и как раз уводит нас от predictability. Более того она заставляет бегать по всем reducer-ам на любой чих. Статичная схема где у каждого action-type-а есть конечная "дестинация" намного проще в отладке и разработке.


      1. bgnx
        12.11.2019 13:42

        Я тоже когда использовал redux к этому пришел. Один экшн — обработка в одном единственном редюсере. И никаких combineReducers. Но я пошел еще дальше. Вместо неудобных switch-ей я просто решил экспортировать функции


        export const createTodo = (state, action) => ...

        а главный редюсер просто импортирует и вызывает


        import * as actions from "./actions"
        
        export const rootReducer = (state, action) => actions[action.type](state, action)

        А потом в отдельных редюсерах я решил вынести болерплейтный код в функции-хелперы — так как любую сложную логику всегда можно разбить на набор crud-операций над отдельными сущностями — то вместо того чтобы писать этот болерплейт дектруктуризации вручную я создал хелперы create(), update(), delete() которые обновляют объекты в нормализированном сторе.
        Например вместо такого кода


        export const createTodo = (state, action)=>{
         const newTodoId = Math.random()
         return {
           ...state,
           users: {
             ...state.users,
             [state.currentUserId]: {
               ...state.users[state.currentUserId],
               newTodoText: "",
               todos: [...state.users[state.currentUserId].todos, newTodoId]
             }
           }
           todos: {
             ...state.todos,
             [newTodoId]: {
               id: newTodoId,
               text: state.users[state.currentUserId].newTodoText
             }
           }
         }
        }

        я стал писать только саму логику а весь болерплейт связанный с деструктуризацией был вынесен в хелперы и перестал смешиваться основной с логикой


        export const createTodo = (state, action)=>{
         const newTodoId = Math.random();
         state = create("todos", newTodoId, {
           id: newTodoId, 
           text: state.users[state.currentUserId].newTodoText
         });
         state = update("users", state.currentUserId, {
            newTodoText: "", 
            todos: [...state.users[state.currentUserId].todos, newTodoId]
         });
         return state;
        }

        А потом я подумал — если каждому экшену соотвествует только один редюсер то зачем логику держать в редюсере — я ведь могу создать 3 редюсера create(), update(), delete() а саму логику держать там где она требуется — например рядом с кнопкой. И это удобно — так как а) получаем меньше действий навигации по исходникам чтобы понять что делает конкретная кнопка б) когда удаляем кнопку не нужно помнить что где-то в другом файле нужно не забыть почистить и удалить лишний редюсер


        <button onClick={()=>
         const newTodoId = Math.random();
         create("todos", newTodoId, {
           id: newTodoId, 
           text: getState().users[getState().currentUserId].newTodoText
         });
         update("users", getState().currentUserId, {
           newTodoText: "",
           todos: [...getState().users[getState().currentUserId].todos, newTodoId]
         })
        }}>
          add todo
        </button>

        Ну а дальше я стал еще больше убирать болерплейт и добавлять удобства — вместо того чтобы передавать хелперам имя таблицы в нормализирвоанном сторе я создал фабрики для таблиц — и вместо create("todos", newTodoId, {..}) получилось todos.create(newTodoId, {...}) — теперь опечататься уже не получится, плюс имеем автокомплит


        А дальше вместо того чтобы возиться с айдишниками я сделал врапперы над объектами которые скрывают болепрейт постоянного вытаскивания объекта по айдишнику — то есть вместо того чтобы получать временный текст тодошки у текущего юзера таким образом


        getState().users[getState().currentUserId].newTodoText

        я просто обращаюсь к геттеру у объекта текущего юзера


        currentUser.newTodoText()

        а внутри геттера newTodoText() будет происходить чтение по айдишнику своего объекта нужного свойства в нормализирвоанном сторе. Ну и добавив к объекту врапперу метод update(), получим удобный способ обновлять поля объектов. В итоге пример с добавлением тодошки стал записываться примерно так


        <button onClick={()=>
         const newTodo = state.todos.create({text: currentUser.newTodoText()});
         currentUser.update({newTodoText: "", todos: [...currentUser.todos(), newTodo]})
        }}>
          add todo
        </button>

        И если на примере тодошки болерплейт связанный с постоянным вытаскиванием по айдишникам не сильно заметен то когда количество сущностей со связями one-to-many становится больше — например юзер может создать папки в папках борды в бордах проекты а в проектах тодошки то чтобы прочитать свойство папки имея объект тодошки нам нужно несколько раз вытаскивать по айдишнику родительские сущности и такой код становится просто нечитаем


        state[state[state[todo.projectId].boardId].folderId].someField

        а если вынести этот болерплейт в объекты-врапперы то получаем вполне себе читабельный код


        todo.project().board().folder().someField

        А дальше я увидел mobx где получаем все то же самое к чему я пришел но проще и эффективней без ненужной иммутабельности которая на тот момент стала лишь технической деталью потому что внешний интерфейс стал напоминать работу с графом объектов связанных между собой ссылками — можно пройтись по ссылкам из одного объекта к другому как например из глубокой сущности к родительской task.project().folder().user().someField, так и обновить нужное свойство на любой глубине — task.update({text: newText})


        1. faiwer
          12.11.2019 14:36

          Собственно да, вы шаг за шагом ушли от redux way в сторону чего-то вроде MobX. Думаю вы бы сэкономили себе много времени если бы использовали его с самого начала. Я тоже до неузнаваемости искажаю работу с redux, избавляясь от того, что я считаю лишним, и добавляя то, что считаю необходимым. Но суть — one way data flow + signals — не изменяю, остаюсь при ней.


          У меня:


          • нет action type констант, т.к. они создают больше проблем чем решают (в приложениях любого масштаба).
          • нет combine reducer-ов, вместо этого — просто древо reducer-ов где каждому action-у задан конкретный reducer-handler, путь к которому определяется по action type.
          • в reducer-ах всегда доступен 3-им аргументом rootStore для чтения
          • immer разумеется, куда без него в 2019, не писать же………… везде
          • actionCreator-ы и их reducer лежат в одних и тех же файлов (никаких import hell), чаще всего селекторы лежат там же
          • для переносимого кода всё это оборачивается в фабрики
          • отдельный велосипед позволяющий избежать привязки к конкретному месту в state tree абстрагируя это на уровне выше. по сути redux код модуля ничего не знает о том, где лежит его стейт, но при этом все его selector-ы работают исправно

          Ну и многое другое. С бухты барахты надо ещё догадаться, что это всё ещё redux. Ведь почти нет boilerplate кода, нет тонны однообразной копипасты в actionCreator-ах (использую фабики POJO actionCreator-ов), нет этой switch-case лапши в reducer-ах, нет десятков путанных файлов, когда 1 функциональность рассеяна по кодовой базе. Много чего удаётся избежать.


          При этом time machine и redux dev tools — работают, immutability — на месте, predictability — даже лучше, one way data flow — на месте. Но несколько больше специально введённых ограничений. В итоге система в большей степени устойчива в хаосу.


  1. hd_keeper
    11.11.2019 12:57

    Вот до чего доводит бездумное следование так называемым паттернам проектирования.


    1. mayorovp
      11.11.2019 14:18

      Если бы они тут ещё были, эти паттерны...