Redux — технология относительно молодая. Чётких правил что, как и где использовать нет.
Есть рекомендации, но и их не все читают.

Очень многие вообще используют Redux исключительно потому что «все так делают», что зачастую сводит его полезность к нулю, или вообще просто бессмысленно усложняет приложение и добавляет ему лишних ошибок.

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

И, так как заботливо разложенные грабли, которые пришлось вычищать из чужого кода, были практически те же самые, мне захотелось рассказать об этом.

Давайте рассмотрим несколько примеров, я их специально брал из разных проектов, они написаны разными людьми из разных стран.

Синхронный action:

export const DISABLE_WELCOME = 'DISABLE_WELCOME';
export function disableWelcome() {
  return {
    type: DISABLE_WELCOME,
    payload: {}
  }
}

и reducer для него:

export default function (state = INITIAL_STATE, action) {
   switch (action.type) {
     // много разных других reducer'ов
  case DISABLE_WELCOME:
    return {
       ...state, auth: {
         loggedIn: true,
         errorMessage: null,
         welcome: false
      }
   };
   default:
      return state;
  }
}

В общем, всё просто и очевидно, никаких подводных камней особо нет.

Теперь посмотрим на асинхронный вызов из того же проекта, в административной части в таблицу добавляется новый вид некой «подписки»:

export function createSubscription(props) {   // Create Subscription
  const request = axios.post(`${ROOT_URL}/subscription`, props,
    {
      headers: { Authorization: getAuthToken() }
    });
    return {
      type: CREATE_SUBSCRIPTION,
      payload: request
  }
}

и простенький reducer

  case CREATE_SUBSCRIPTION:
    return {...state, subscription:action.payload};

Всё сделано совершенно по инструкции. В качестве action payload передаётся promise, при инициализации redux'a в него добавляется библиотечное middleware redux-promise, reducer вызывается после получения ответа от сервера, вроде как всё нормально.

Но сразу бросаются в глаза две проблемы:

  1. Reducer вызывается только после получения ответа от сервера, а хорошо бы звать его два раза, в начале запроса и в конце (например чтобы сразу добавить в таблицу значение, которое создал пользователь, а не ждать ответа от сервера и только после этого обновлять UI — это чисто внешне выглядит значительно лучше)
  2. Те же самые строки с функцией getAuthToken() повторяются в том же файле actions.js около 40 раз.

В данном проекте первая проблема была решена достаточно брутальным образом, программист просто вызывал два action подряд, один с простым объектом для обновления UI, второй с промизом.

Варварство? Варварство.
Работает? Работает.

Более правильный и чаще встречающийся подход предполагает использование асинхронного middleware которое бы вызывало бы reducer два раза, при вызове action и после получения ответа:

Cкучный код для promise middleware практически из примера
export default function promiseMiddleware({ getState }) {
  return next => action => {
    // проверяем если наш action payload это Promise, то
    if (isPromise(action.payload)) { 

      const { type, payload, meta = {} } = action;
        // сразу зовём reducer с meta.sequence: 'begin' 
      next({ 
        ...action,
        payload,
        meta: {
          ...meta,
          sequence: 'begin' 
        }});

      payload.then(
          // по завершению reducer с meta.sequence: 'complete' 
        result => next({ 
            ...action,
            payload: result,
            meta: {
              ...meta,
              sequence: 'complete' 
            }}
          ))
          // и если была ошибка, то reducer с meta.sequence: 'error' 
        .catch(err => next({ 
            ...action, 
            payload: err,
            meta: {
              ...action.meta, 
              sequence: 'error' 
          }}
        ));

    } else {
      next(action);
    }
  };
}


То есть что мы здесь получаем: reducer вызывается два раза, один раз вначале (meta.sequence === 'begin') и в конце при успешном завершении запроса (sequence: 'complete') или при ошибке (sequence: 'error').

Пару раз я встречался с выносом сюда же функций для управления аутентификацией, что-то вроде такого:

  const token = getState().getIn(['session', 'token']);
  if (token && typeof action.payload.set !== 'undefined') {
     action.payload.set('Authorization', token);
 };

Понятно, что выглядит не очень аппетитно — привязано к конкретному месту хранения токена в конкретном store (в данном случае это ещё и Immutable), привязано к конкретной библиотеке для http запросов, если в качестве action payload будет передана какая-нибудь другая функция с методом или свойством set (а название, признаемся, не самое редко встречающееся), последствия могут быть самыми непредсказуемыми, ну и так далее. Однако, тоже работает.

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

Первая проблема возникает, когда нужно скомбинировать несколько асинхронных функций, причём нужно чтобы они вызывали разные reducer'ы.

Другая проблема, когда у функции не один callback, а несколько. Всё, промизом здесь уже не обойдёшься. В результате я вижу dispatch, передающийся в функцию createAction чтобы создавать при поступлении нового callback'a ещё больше action'ов. Или вижу как весь кусок с websocket'ами, например, переезжает в какой-нибудь реактовский компонент из action'ов, в результате чего впоследствии несколько дней тратится на то чтобы понять, почему вдруг всё иногда перестаёт работать (а всего лишь компонент решил перемонтироваться).

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

Что делать если нужно много callback'ов...
const disconnect = createAction('DISCONNECT_MESSENGER',
    () => {
      client.unsubscribe('/messenger')
      client.disconnect();
    });

const wasDisconnected = createAction('WAS_DISCONNECTED_MESSENGER',
  () => client.unsubscribe('/messenger'));

const receive = createAction('RECEIVE_MESSAGE');

const connect = createAction('CONNECT_MESSENGER',
    ( token, dispatch ) => client.connectAsync({ auth: { headers: { Authorization: token } } })
       .then(() => {
          client.onDisconnect((willReconnect, log ) => dispatch(wasDisconnected({willReconnect, log})))
          return client.subscribeAsync('/messenger', message => dispatch(receive(message)))
      }),
    ( payload ) => payload );


Хендлер внутри реактовского компонета. Смотреть осторожно, может быть причинён вред здоровью
    dispatch(editStory(formData, id)).then((results) => {

      if (results.payload.data.error === true) {
        alert.error('Error', results.payload.data.message)
      }
      else if (results.payload.data.error === false) {

        this.setState({ loadingClassName: '' });
        this.close();
        this.props.fetchStory();
        dispatch(push('/story'));
        alert.success('Success', results.payload.data.message)
      }
    }).catch((err) => {
      dispatch(push('/story'));
      alert.error('Error', 'Oooops! Looks like something went wrong. Please try again after sometime.');
    })

Да, .then() от dispatch(). Оно работает, кстати…

Да, есть варианты использовать что-нибудь для создания последовательности action'ов или Thunk, но почему-то в реальности каждый раз я вижу именно такое.

Что делать


Отправляем всё в middleware. Целиком.

Action — это действие. Действие — это отправить данные на сервер или получить данные с сервера, а не «создать promise» — promise, как и библиотека для асинхронных запросов, будь это хоть fetch, хоть axios, хоть superagent — это всё внутренняя кухня, давайте уберём её из логики:

const requestMiddleware = 
  ({ getToken } = {}) => // Давайте передадим тут функцию для получения токена, всего один раз
  ({ getState }) => // И у нас здесь есть полный доступ к state!
  next => 
  action => {
         // обозначим то, что мы хотим обратиться именно к нашему middleware
         // с указав в payload use: 'request'. Можно придумать что-нибудь получше
    if (action.payload && action.payload.use === 'request') { 
      const { 
        payload, 
        type,
        meta = {} 
      } = action;

      const {
        url,
        method,
        data,
        query      
      } = payload;

      // И нам без разницы какая библиотека этот "request"!
      // Заменим здесь - заменится для всех запросов.

      const myRequest = request(method, url);  

      if (typeof getToken === 'function' ) {
        // токен прикладываем тоже здесь. Так как нам удобно. 
        // А для его получения используем внешнюю функцию, чтоб особо не хардкодить
        myRequest.set('Authorization', getToken(getState()));
      }

         // Выставляем всё что надо

      if (query) {
        myRequest.query(query);
      }

      if (data) {
        requestObject.send(data);
      }
      

      myRequest.then(response => next({  // Зовём reducer если всё успешно
          ...action,
          payload: response.body,
          meta: {
            ...meta,
            sequence: 'complete' 
          } 
        })          
      ).catch(err => next({ // Зовём reducer если всё плохо
          ...action,
          payload: err, 
          meta: {
            ...action.meta, 
            sequence: 'error' 
        }}
      ));

      next({ // Зовём reducer в самом начале
        ...action,
        meta: {
          ...meta,
          sequence: 'begin'
        }});

    } else {
      next(action); // Не забываем об остальных
    }
  }

Теперь добавим наш middleware:

const options = {
  // наша функция для получения токена
  getToken = store => store.getIn(['session', 'token'])
}
const store = applyMiddleware(
  requestMiddleware(options)
)(createStore)(reducer);

И всё. Можно закрывать этот файл навсегда и пользоваться:

const myAction = ({ 
  type: 'GET_REMOTE_URL',
  payload: {
      use: 'request',
      method: 'GET',
      url: 'https://your.site/api'      
    }
});

Не сомневаюсь, что всё можно сделать и изящнее, и удобнее.

Но это самая простая часть, а если нам надо добавить ещё какие-то callback'и? Например, мы загружаем длинный файл и нам нужно обновлять store в зависимости от прогресса загрузки?

Да не вопрос — если наша библиотека для запросов позволяет добавить, например, event on('progress') просто пишем:

const handleProgress = progress => next({ 
  ...action,
  request,
  payload: {
    progress: progress.percent
  },
  meta: {
    ...action.meta, 
    sequence: 'progress' 
  }
})

myRequest.on('progress', progress => handleProgress(progress))

Всё. Готово. Проверяйте в своём reducer'e sequence === 'progress'

Нужно добавить socket.io или что-нибудь подобное? Пожалуйста, точно так же.
Создайте, на все события вызывайте next() с нужными вам параметрами, и всё.

И точно так же можно поступать с абсолютно любыми асинхронными функциями, которые вы вызываете более одного раза (всё-таки если один раз — смысла особого нет).

Очень удобно сделать отдельный middleware для задержек и таймеров — всё-таки всякие setInterval() в аккуратном коде аккуратного компонента смотрятся довольно чужеродно.

В ReactNative вы можете отправить туда Alert или вызов какого-нибудь нативного компонента типа react-native-image-picker — и если вдруг вам срочно придётся заменять его на какой-нибудь react-native-image-crop-picker (а подобные необходимости обычно возникают чуть чаще, чем хотелось бы...) — вы замените его всего в одном месте, а не в десятке.

Сделайте разные middleware для LocalStorage для веб-страницы и для AsyncStorage в ReactNative, но сделайте способ обращения к ним одинаковым — и вы сможете использовать одни и те же action и для сайта и для приложения.

В общем, в action оставляем только логику. Чем меньше там конкретной реализации — тем лучше. Оставим всю грязную работу для middleware.

Я собрал несколько своих middleware, которыми я пользуюсь постоянно, в библиотечку Redux Kittens, может быть кому-нибудь тоже пригодится.

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


  1. vasIvas
    10.10.2017 16:35
    +1

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


  1. parakhod Автор
    10.10.2017 17:44
    +2

    Если у меня в коде стоит два диспатча рядом, я начинаю подозревать что я что-то делаю не так. Если во всём коде эти два диспатча ходят исключительно вместе, то подозрение перерастает в твёрдую уверенность.
    Что же касаемо частого рендера, то в реакте это за грех не считается. Да и рендер-то не настоящий же. Ну а на крайний случай всегда shouldComponentUpdate имеется.


    1. vasIvas
      10.10.2017 17:59
      -2

      Безусловно могут быть ситуации, в которых два диспатча, это следствие непродуманной архитектуры построения логики. Но я такие моменты даже не рассматриваю. Я, как пример, могу привести запись в хранилище и в localStorage.Данные одни, а редюсеры, как минимум два. В такой ситуации и нужно делать два диспатча. Не смешивать же редюсеры в кучу? И какой-бы быстрый рендер не был, он все равно будет затратней чем два диспатча в десятки, если не сотни раз. нужно уметь выявлять узкие места и думать только о них.


      1. parakhod Автор
        10.10.2017 18:58

        Зачем два диспатча, если вы пишите одно и то же? localStorage вообще синхронный, от AsyncStorage при записи тоже никакой асинхронности обычно не требуется — разместите функцию записи хоть в функции создания action, хоть в редюсере (я предпочитаю второй вариант) — и всё прекрасно будет работать с одним dispatch.
        А экшны имеет смысл упрощать не для ускорения — какое тут ускорение — а для уменьшения количества возможных логических ошибок.


        1. vasIvas
          10.10.2017 19:25

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


          1. parakhod Автор
            10.10.2017 19:36

            «В actionCreator нельзя» — почему нельзя-то? Не очень изящно, но работать будет.


            1. vasIvas
              10.10.2017 19:48
              +1

              Потому что, это «Варварство». redux это слой между хранилищами-сервисами и реактом. Даже запросы в actionCreator делать неправильно. Логику нужно реализовывать «где-то», а инициализировать уже в actionCreator. А с хранилищами, к каковым относится и localStorage, нужно работать с помощью reducer.

              К тому же я могу расширить пример ситуаций, когда при получении данных от представления, нужно не просто их сохранить, но и на их основе получить результат выражения и его сохранить в localStorage. Тут на лицо один actionCreator, который вызывает два других. Только вопрос, где именно будет вызывать dispatch, немного может не уложится в мой первый неудачный пример. потому что, dispatch может вызываться в двух других actionCreator, но при этом, это все равно будет два диспатча подряд.

              И ещё стоит добавить, что если вы сделаете правильно и вынесите запросы из actionCreator, то не придется сорок раз токен получать. actionCreator вообще не должны знать о токенах.


              1. parakhod Автор
                10.10.2017 20:40

                И ещё стоит добавить, что если вы сделаете правильно и вынесите запросы из actionCreator, то не придется сорок раз токен получать. actionCreator вообще не должны знать о токенах.

                Собственно у меня вся заметка как раз об этом.

                Что же касается «делать неправильно» — я тоже обычно придерживаюсь такого подхода, однако когда у меня возникает дилемма написать что-то прямолинейно и «неправильно» в две строчки или «правильно», но в пятьдесят, при том что делать это будет одно и то же и никаких подводных камней не предвидится, интуиция, опыт и лень подсказывают мне использовать первый вариант.
                Тот же storage, возьмём для примера AsyncStorage из реакт-натива — он асинхронный, и он уже готовый промиз — я могу городить огород пятнадцатью различными «правильными» способами, а могу кинуть AsyncStorage.get() в прямо payload экшна, а всякие AsyncStorage.set() останутся в редюсерах. Пусть это триста раз будет неправильно с чьей-то точки зрения, но я не пложу лишнего кода только ради «правильности» и тем самым в реальности уменьшаю вероятность возникновения ошибок.


                1. vasIvas
                  10.10.2017 20:49

                  AsyncStorage.get() в прямо payload экшна, а всякие AsyncStorage.set() останутся в редюсерах

                  Более правильно сделать в actionCreator, так как все с чем Вы работаете с помощью reducer, хранится в состоянии приложения. А как известно в actionCreator помимо dispatch идет ещё и state. То есть, в некоторых случаях, даже обращение к хранилищу не требуется.


          1. ElianL
            11.10.2017 15:19

            reducer'ы должы быть чистыми функциями, нельзя в них писать что-то в localStorage.


            1. parakhod Автор
              11.10.2017 16:51
              -1

              Почему нельзя?
              Это противоречит заповедям Моисея, или правилам пожарной безопасности?
              В чём смысл расовой чистоты функции редюсера если она в любом случае возвращает новый state?
              Какой смысл мне городить listener'ы над этим state, или ещё хуже в реакте отслеживать componentWillReceiveProps, всего лишь чтобы сделать какое-то простое действие к стейту не относящееся, но происходящее исключительно в тот же самый момент если он обновляется определённым образом?
              А если я не запись в localStorage сделаю, если я какой-нибудь alert оттуда покажу, что, меня нужно разжаловать из программистов в филологи?


              1. faiwer
                11.10.2017 20:28

                Всякий раз делая шаг в сторону, важно понимать его последствия. Некоторые side-effect-ы могут сильно усложнить жизнь в будущем, когда всё почему-то очень сильно запутается и станет работать не просто и прямолинейно, а чёрт знает как, хотя вроде бы… ой. А это обязательно случится с течением времени. Вопрос лишь в сроках.


                По сути, имея столько ограничений, сколько накладывает на наши плечи связка redux+react, при этом при достаточно сложных задачах (большие и сложные приложения), нам так или иначе приходится делать что-то "противоречащее заповедям". И если мы понимаем для чего это, понимаем, что в данном случае это разумное и обоснованное решение, которое даёт такие преимущества, что недостатками можно пренебречь — то ок. Мы делаем это со знанием дела.


                А если это просто, потому, что лень. То туды её в качель. Скажем если возникает необходимость писать что-то в localStorage из редьюсеров в разных местах… То может сделать 1 .substribe и некий manager-обёртку, которая позволит всё это дело систематизировать и не переусложнять элегантную простую схему, заложив мину туда, где её никто не ждал...


                Так, мысли в слух.


                1. parakhod Автор
                  11.10.2017 22:12
                  +1

                  Здравый смысл, здравый смысл и ещё раз здравый смысл.
                  Я получаю токен с сервера и сохраняю его в storage, один раз в одном месте — да, в редюсере, да мне так нравится.
                  Считываю я его тоже один раз в одном месте, при инициализации — ну в чем тут даже теоретически может возникнуть проблема?
                  Требование неукоснительно соблюдать правила ради самих правил немножко смахивает на армию и, как и в армии, кроме зашкаливающего идиотизма обычно ни к чему не приводит.


                  1. staticlab
                    12.10.2017 08:35

                    А как вы тестируете редьюсеры с побочными эффектами?


                    1. mayorovp
                      12.10.2017 09:18

                      Можно localStorage замокать подменить.


              1. Nerlin
                11.10.2017 22:01

                Потому что это нарушает SRP.

                Почему в таком случае не выделить функцию, которая будет запускать этот необходимый код, а затем вызывать dispatch и изменять состояние приложения? Зачем мешать логику изменения текущего состояния с кешированием или другими процессами?


                1. parakhod Автор
                  11.10.2017 22:29
                  -2

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

                  Ну серьёзно, в чем смысл каждую концепцию доводить до абсурда? Тем более что эти концепции склонны проходить путь вначале от рекомендаций до догм, а потом до устаревших и нерекомендуемых к использованию в среднем лет за 20?

                  Предложенный вами подход в приложении к простейшей задаче отправки запроса на сервер, получения токена и сохранения его одновременно в state и storage увеличивает количество необходимого кода в разы. А чем больше кода, тем больше ошибок.
                  Всё-таки принцип бритвы Оккама гораздо старше, и вполне доказал свою состоятельность.


                  1. Nerlin
                    12.10.2017 00:35
                    +1

                    До абсурда доводите Вы, приводя такие примеры. Более верной аналогией в Вашем же примере было бы: «Хотите в туалет — не срите там же, где работаете». Почему в квартире есть отдельные места для каждого занятия — зал, кухня, спальня, туалет, а не одна комната для всего? Возможно, Вам было бы удобно одновременно стоять в душе и готовить яичницу, но это не значит, что это будет действительно эффективно.

                    Ближе к делу. Кода не увеличится в разы, добавится одна лишь функция, занимающая строки две, по сравнению с тем же кодом, который расположен будет у Вас в reducer с нарушением SRP, зато reducer при этом останется чистым. Изменится лишь вызов. К тому же, Вы рассматриваете лишь тот случай, когда сохранять одновременно нужно и в store, и в storage, а если нужно что-то сохранить лишь в store с тем же action или если нужно применить sessionStorage или вообще что-то иное почему-то нет. Может конечно у Вас весь store всегда хранится в localStorage, я не знаю, но смысла в этом я не вижу никакого, зачем эта зависимость в принципе нужна в reducer'е? Человек, не знакомый сразу с Вашим проектом, должен будет изучить эту особенность реализации и знать, что что-то изменяя в state, он вносит изменения и в localStorage, лучше эту особенность выделить явно в отдельную функцию.


                    1. parakhod Автор
                      12.10.2017 10:24

                      Да, я привожу абсурдный пример с бытовами аналогиями чтобы показать абсурдность догматичного восприятия каких-то принципов. Принципы тоже живые люди придумали, и я себя, лично, не считаю сильно глупее их.
                      Теперь попрошу вас внимательно перечитать то, что я написал о конкретно моей задаче (не такая уж редкая задача, кстати).
                      В storage сохраняется только токен (иногда ещё информация о текущем пользователе).
                      Данные эти приходят после выполнения определённого (и единственного) запроса к api.
                      Запрос делается с помощью middleware, которое собственно и выполняет запрос, и вызывает редюсер два раза — вначале и по окончанию.
                      Итак, внимание вопрос, где по-вашему должна «добавиться ещё одна функция занимающая строки две»?
                      Нажимается кнопка «login» — там никакого токена нет.
                      Диспатчится экшн который вызывает middleware для запроса api — там тоже токена нет.
                      Токен я в первый раз увижу в middleware по завершении запроса — но middleware не особо должно интересовать что за данные оно получило, оно не по данным — оно за запрос отвечает.
                      А потом всё, редюсер. И первый и последний раз в чистом виде я токен вижу там.
                      Я могу получить его и сохранить и в state и забекапить в storage (да, кстати у меня это делается не то что отдельной функцией, а отдельной библиотекой — она поддерживает и localStorage и реакт-нативовский AsyncStorage — не вижу смысла писать почти одинаковый код два раза для разных платформ.)
                      Так что если не хватать его там — единственный способ как это ещё сделать — это вешать listener который будет следить за изменением токена в state и сохранять его в storage.
                      И зачем? Он будет проверять каждое шеаеление стейта. Если какой-то другой редюсер по ошибке испоганит стейт он честно запишет в storage новый мусор.
                      Тормоза и потенциальные источники ошибок исключительно ради фанатичного соблюдения принципа которому лет в три раза меньше чем я программированием занимаюсь?
                      Извините, нафиг.


                      1. staticlab
                        12.10.2017 11:18

                        Сегодня надо сохранить в LocalStorage, завтра ещё и на сервер. При этом не забыть отправить нотификации об успехе или ошибке. Подобные операции в редьюсере делать уже нельзя, надо рефакторить. Кстати в redux-saga для подобных операций на экшен навешивается сага-обработчик, которая сама по себе позволяет делать асинхронные операции, вызывать внешние API и порождать новые экшены.


                        1. parakhod Автор
                          12.10.2017 11:33

                          «Сегодня надо A, завтра ещё и B» — в своё время такой подход к программированию назывался «позолотой». Придумывание теоретической возможности необходимости использования дополнительного функционала в будущем для оправдания ненужного усложнения кода сейчас.
                          Макконнел сильно осуждал.


                          1. staticlab
                            12.10.2017 11:37

                            Но вы же своё решение предлагаете использовать другим, не так ли? Тем более, что сильного усложнения ведь и не предлагает никто. А упрямое упрощение вот прямо здесь и сейчас, кажется, ничуть не лучше неоправданного усложнения.


                            1. parakhod Автор
                              12.10.2017 11:46

                              Я никому ничего не предлагаю.
                              Я рассказываю про middleware и свой опыт с ним.
                              Вы знаете, я давно вышел из того возраста когда мне были необходимы признание и обратная связь — если кто-то захочет воспользоваться моим опытом или чем-то что я выложил в открытый доступ — пожалуйста, берите, не жалко.
                              Кто-то может мне аргументированно возразить — тоже, выслушаю с интересом. Учиться надо постоянно и я, если надо, признаю свою неправоту даже с благодарностью — это в первую очередь мне самому будет полезно.
                              Однако когда вся аргументация сводится к «потому что так не положено», а не положено потому что какой-то авторитет когда-то так сказал — ну извините, это не аргумент.
                              Призыв к «чистоте функций» так же в абсолютности своей нелеп как и призыв к отказу от глобальных переменных, от goto, к отказу от функций с полным переходом на методы классов, отказу от короткого наименования переменных, обязательному использованию нотации считаемой очередным гуру единственно правильной, обязательной типизации переменных, отказу от обязательной типизации переменных и т.п. — много я уже этого видел, извините.


                              1. staticlab
                                12.10.2017 11:48

                                Хорошо, но вы же сами только что аргументировали Макконнеллом и ранее Оккамом...


                                1. parakhod Автор
                                  12.10.2017 11:54

                                  «Знать» и «слепо использовать» — разные вещи.
                                  Если я сам не могу переформулировать простым языком идею высказанную каким-то авторитетом и не обосновал в своё время сам для себя её правильность — тут уж извините, значит либо эта идея до меня ещё не дошла, либо это не столь хорошая идея.
                                  И опять же — всё это для меня не догма. Я всегда стараюсь руководствоваться исключительно здравым смыслом.


      1. sainttechnik
        12.10.2017 09:47

        Почему бы не использовать еще один middleware, который будет заниматься синхронизацией нужного куска стора с LocalStorege? Вся работа по синхронизации вынесена «куда-то». Не нужно писать два dispatch, не нужно писать сайд эффекты в редюсере, не нужно писать логику сохранения в action.


        1. parakhod Автор
          12.10.2017 11:37

          В принципе идея неплохая, однако она всё-таки тоже немного отдаёт усложнением. А где усложнение — там всегда ошибки и глюки.


  1. staticlab
    10.10.2017 17:48
    +2

    Кажется, это те же эффекты из redux-saga, только вид сбоку.


    1. parakhod Автор
      10.10.2017 19:05

      Глянул краем глаза — ну да, суть та же. Правда на мой вкус это излишнее преумножение сущностей, если всё можно сделать и стандартными средствами, а особой выгоды ни в размере кода ни в его читаемости я не вижу.
      В общем, мой внутренний Оккам не одобряэ ))


      1. staticlab
        10.10.2017 19:25

        Суть redux-saga с одной стороны — замена прямого вызова асинхронных операций вызовом эффектов (т.е. аналогично вашим миддлварам), а с другой стороны — использование генераторов, позволяющих компоновать асинхронные операции аналогично async/await, но с возможностью пошагового тестирования.


      1. raveclassic
        12.10.2017 01:19

        Ну где ж стандартными, у вас самопальная мидлварь.
        К слову, ваше решение поддерживает отменяемость и рейсы? А тестируется так же удобно как саги?


        1. parakhod Автор
          12.10.2017 10:01

          Миддлварь — стандартный подход, ну а моё отношение к преумножению сущностей я уже высказал.
          Что же касается самопальности — ну да, я сделал то что надо именно мне и сделал под себя — «универсальные» решения чаще всего страдают от того, что они призваны удовлетворять всех, и поэтому полностью не могут удовлетворить никого.
          Отменяемость поддерживает, что такое рейсы не знаю.
          В тестировании именно как тестировании в отношении фронтенда или мобильных приложений у меня необходимости никогда не возникало, это ж не api. У меня достаточно удобно настраиваемый консольный вывод дебажной информации — мне этого достаточно.
          Опять же — все свои библиотеки я пишу исключительно для себя лично и до последнего времени я их даже на гитхаб не выкладывал. Это решение удобное лично для меня — а в этой заметке я вообще пишу даже не о решении, а о подходе к нему. И у меня нет ни малейших сомнений в том что процентов 80 тех, кто пользуется редаксом, о возможности такого подхода даже и не задумывалось.


  1. 7iomka
    11.10.2017 08:05

    Советую посмотреть в сторону redux-logic. Автор проникнут Вашей болью, и рассмотрел все минусы подходов, используемых разными либами, и выбрал на мой взгляд золотую середину)


  1. Fedcomp
    11.10.2017 11:42

    Подобие этого middleware был еще в redux real-world example (в 2015 году точно). Что забавно, поначалу у нас было только начало/конец/эррор (только полноценные события вроде BEGIN_USER_LOAD, END_USER_LOAD, USER_LOAD_ERRROR вместо meta значения), потом тоже пришлось прилепить коллбэки (saga не смотрел).


    1. parakhod Автор
      11.10.2017 12:03

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

      Лично мне ближе вариант, когда мне приходится меньше писать — я крайне ленив.
      То есть я в своё время написал пару утилиток и для экшенов и для редюсеров — я давно не указываю имена редюсеров типа вот этих USER_LOAD, если я обращаюсь к экшн userLoad то у меня зовётся редюсер по имени USER_LOAD, но я его и в сторе не указываю явно, я там пишу что-то типа:

      createReducers({
        userLoad: {
          begin: state => state.set('userLoading', true),
          end: state => state.set('userLoading', false).set('userLoaded', true),
          error: {
            401: state => state.set('error', 'authError'),
            default: state => state.set('error', 'someGeneralError')
          }
        }
      })
      

      и утилитки всё распихивают за меня, переваривая правильные type и meta.

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


  1. Mixail
    11.10.2017 15:36

    Так же можно посмотреть на redux-api-middleware


    1. parakhod Автор
      11.10.2017 17:11

      Их много таких готовых.
      Но моя цель была не дать ссылки на какие-то существующие библиотеки, а показать, что каждый при необходимости может легко написать своё middleware конкретно для своей задачи, и что это очень просто и никакие искусственные усложнения при этом совсем не требуются.


  1. LiguidCool
    12.10.2017 15:24

    «И отправим Redux туда, где ему самое место — в топку! =D» ©
    Ну а если серьезно… Да это модно и потому redux пихают бездумно в каждый проект, хотя на самом деле он в принципе нужен далеко не всегда и писать на том же React можно вполне без него. Более того это некислое усложнение кода зачастую и вместо его облегчения начинают наоборот лезть костыли.


    1. parakhod Автор
      12.10.2017 15:29

      Чистейшая правда. И в 70% случаев redux можно заменить локальными стейтами или стейтом корневого компонента, раздавая его как пропсы вниз.

      К сожалению, сейчас слишком многие привыкли усложнять на ровном месте.

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