Redux Action Creators. Без констант и головной боли
Всем привет! Эта статья будет полезна тем, кто устал использовать constants в Redux (частично показано на превью выше). Под катом я покажу очередной возможный велосипед и как на нем кататься.
Модуль + документация (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#.cj2jzhbf726 сентября 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-actions26 сентября 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 });