Redux Action Creators. Без констант и головной боли

9221b9b4964341dbacf17cb0c53c2561.jpg


Всем привет! Эта статья будет полезна тем, кто устал использовать constants в Redux (частично показано на превью выше). Под катом я покажу очередной возможный велосипед и как на нем кататься.



1b19a26afe4c47f18538fa4ce6c3d40e.png


Модуль + документация (https://github.com/pavelivanov/redbox)

Введение

Использование Redux предполагает наличие экшнов (actions) и редьюсеров (reducers), а также констант (constants), которые используются для связи экшнов с редьюсерами посредством передачи type (типа экшна).


Пример использования:


const ADD_TODO = 'ADD_TODO'

export {
  ADD_TODO
}

import { ADD_TODO } from 'constants'

export const addTODO = () => {
  return (dispatch) => {
    dispatch({
      type: ADD_TODO,
      item
    })
  }
}

const ADD_TODO = 'ADD_TODO'

const initialState = {
  TODO: []
}

export default (state = initialState, action) => {
  switch (action.type) {

    case 'ADD_TODO':
      return {
        ...state,
        TODO: [
          ...state.TODO,
          action.TODO
        ]
      }

    default:
      return state
  }
}

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


Простые Reducers

В Redbox я избавился от этих проблем. Все что вам нужно — это создать экшн и использовать его. Все.


Пример того же кода (выше) с использованием Redbox:


import { createAction } from 'redbox'

export const initialState = {
 TODO: []
}

export const addTODO = createAction((state, payload) => {
  return { 
    ...state, 
    TODO: [ 
      ...state.TODO, 
      payload 
    ] 
  }
})

Принципиальное отличие: для передачи начального state делается экспорт из файла.


Request Actions

Что касается request экшнов, Redbox предлагает большое кол-во сахара. Пример использования:


Создаем экшн:


// actions/users.js

import { createAction } from 'redbox'

export const getFeed = createAction({
  endpoint: '/api/users/me/posts',
  method: 'GET'
})

Вызываем созданный экшн:


// containers/Users/Feed.js

import actions from 'core/actions'

actions.users.getFeed({
  subset: 'posts'
})

В результате вызова getFeed в state будет:


{
  users: {
    posts: {
      pending: false,
      data: RESPONSE_BODY,
      error: null
    }
  }
}

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


У каждого subset есть 3 состояния:


1) начало запроса { pending: true, data: null, error: null }
2) запрос выполнен { pending: false, data: RESPONSE_BODY, error: null }
3) запрос выполнен с ошибками { pending: false, data: null, error: RESPONSE_ERROR }


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


Подробнее о createAction

Для отправки запросов внутри используется superagent. createAction в опциях принимает почти все параметры, которые используются в superagent.


Основные опции:


params

Любой ключ из опций может быть функцией, в этом случае одним из аргументов этой функции будет объект params. Исключениями являются onResponse и onError


endpoint


URL запроса.
Пример использования с params:


export const getFeed = createAction({
  endpoint: ({ userId }) => `/api/users/${userId}/posts`,
  method: 'GET'
})

getFeed({
  params: {
    userId: 100
  }
})

subset


Название ключа, в котором будут храниться данные в state. Стоит учесть, что полный путь будет строиться по схеме: НАЗВАНИЕ_ФАЙЛА.subset


modifyResponse


Хендлер для редактирования ответа от сервера. Принимает два аргумента — объект Response от сервера и params. Ожидает возвращение нового объекта данных. При этом необходимо возвращать response.body


modifyState


Хендлер для изменения непосредственно state. Принимает два аргумента — весь объект State и params. Может быть полезно для изменения отдельных частей хранилища при использовании одного экшна. Использовать с осторожностью!


onResponse


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


onError


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


Процесс инициализации Redbox

Я решил не писать на русском процесс инициализации, все это есть в документации на странице репозитория, НО, если будет достаточно желающих — я могу расширить статью, добавив такое описание.


Заключение

Буду рад, если мой модуль окажется полезен. Открыт для вопросов, замечаний и предложений по расширению функциональности. Исходный код модуля, доступен в GitHub репозитории

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

  • 26 сентября 2016 в 13:19 (комментарий был изменён)

    0

    Я не понял. А где редьюсер? Или как его создать?
    • 26 сентября 2016 в 13:23

      –1

      Что вы понимаете под редьюсером? По факту обычные редьюсеры в явном виде скрыты под капотом. Если вам нужен метод для изменения state, то в статье есть пример:
      import { createAction } from 'redbox'
      
      export const initialState = {
       TODO: []
      }
      
      export const addTODO = createAction((state, payload) => {
        return { 
          ...state, 
          TODO: [ 
            ...state.TODO, 
            payload 
          ] 
        }
      })
      

      Это и есть экшн + редюсер. При вызове addTODO ('do some stuff') в state добавится этот элемент.
      • 26 сентября 2016 в 13:37 (комментарий был изменён)

        0

        Это конечно хорошо, но я хочу иметь ActionReducer\, как это получить? (чтобы привязать хранилище в ангуляре)
  • 26 сентября 2016 в 14:23

    0

    тут недавно еще это проскакивало https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da#.cj2jzhbf7
    • 26 сентября 2016 в 16:08

      0

      Ага, уже были попытки изобрести похожий велосипед: https://github.com/erikras/ducks-modular-redux
  • 26 сентября 2016 в 14:45

    –2

    Уже был RedBox в контексте React. Некрасиво названия «красть».
    • 26 сентября 2016 в 14:47

      0

      да, хорошо бы переименовать
    • 26 сентября 2016 в 14:48 (комментарий был изменён)

      –1

      Эммм, название я не крал, не надо обвинять плз… я название выбираю по свободности в npm https://www.npmjs.com/package/redbox. Как видите оно пренадлежит мне…

      Хабр как всегда… лишь бы обосрать, а не написать по делу. Можно придраться еще к тому что я использовал 'red' в начале названия… идиотизм

      • 26 сентября 2016 в 15:03

        0

        В чем новизна ваших идей? https://github.com/acdlite/redux-actions
        • 26 сентября 2016 в 15:15 (комментарий был изменён)

          0

          Явной новизны самой идеи нет. Естественно такую задачу как упрощение создания редьюсеров в Redux решали и до меня. Ваш пример я посмотрел (поверхностно). В нем все равно есть намек на типы. Нет встроенного решения для экшнов, т.е. это голые редьюсеры, над которыми вам по старинке придется создавать экшены и передавать данные в созданный редьюсер… Мое решение как минимум изящнее. Что касается возможных сложностей с реализацией частных случаев при использовании Redbox, как я и писал в статья, я готов к предложениям и критике… модуль новый, сырой и нуждается в доработках (ессесно).
          • 26 сентября 2016 в 16:02

            0

            А я всегда думал, что сначала надо обкатать модуль на нескольких проектах, сложнее TODO List. Стабилизировать и только потом идти в сообщество в поиске фидбека и контрибьютеров.
            • 26 сентября 2016 в 16:04 (комментарий был изменён)

              0

              Модуль используется сейчас в реальном проекте, работает стабильно. Возможно я некорректно выразился в прошлом комментарии: имелось ввиду, что бывают частные случаи, которые (возможно, у меня такого не было) будет сложно решить используя мой модуль.
              • 26 сентября 2016 в 16:13

                0

                Я бы для демонстрации модуля сделал бы не большое приложение и выложил бы его исходники. И написал бы об этом пост. Где, как раз, рассказал и показал как оно работает и какие плюсы.
                Мне кажется у всех могло бы возникнуть меньше вопросов или более конкретизированные.
                Плюс многие из нас код читают лучше чем статьи :)
                • 26 сентября 2016 в 16:14 (комментарий был изменён)

                  0

                  Вы правы, спасибо за совет. Постараюсь на днях сделать подробный пример с описанием. Правда как это оформить? Как апдейт к статье?
    • 26 сентября 2016 в 16:04

      0

      Какой миленький, прям ностальгия по php первых версий, где ошибки прям пользователю в лицо вываливались. Истинно история по спирали.
  • 26 сентября 2016 в 16:05

    0

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

    А к вопросу стрёмных текстовых констант вот — https://github.com/pauldijou/redux-act

    • 26 сентября 2016 в 16:10 (комментарий был изменён)

      0

      Это хорошее замечание. Вы отчасти правы. Но. Что вам мешает создать редьюсер и использовать его в нескольких экшнах? Или создать экшн и в его onResponse вызывать несколько редьюсеров?
      В целом я не ушел далеко от стандартов, просто упаковал их в сахар.
      • 26 сентября 2016 в 16:23

        0

        Не до конца понимаю, что вы предлагаете. Можете пример кода на RedBox набросать для обоих случаев? Только для синхронных экшнов, без request, пожалуйста.
        • 26 сентября 2016 в 16:30 (комментарий был изменён)

          0

          import actions from 'core/actions'
          
          export const doMultipleActions = createAction((state, { foo, bar }) => {
            actions.reducersFolderName.setFoo(foo)
            actions.reducersFolderName.setBar(bar)
          })
          

          export const initialState = {
            foo: null,
            bar: null
          }
          
          export const setFoo = createAction((state, payload) => ({ ...state, foo: payload }))
          export const setBar = createAction((state, payload) => ({ ...state, bar: payload }))
          

          doMultipleActions({
            foo: 1,
            bar: 2
          })
          

          Это, конечно, выглядит не совсем корректно с точки зрения использования редьюсера, но работать будет как часы.
  • 26 сентября 2016 в 16:32 (комментарий был изменён)

    0

    Что-то я в простых событиях не вызываю диспатч, только в асинхронных. Код ниже прекрасно работает
    export const testAction = () => ({
        type: TEST_ACTION
    });
    

© Habrahabr.ru