Скажите, люди, я один испытываю небольшой душевный зуд
от необходимости писать нечто вот эдакое? :
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'
export const EDIT_TODO = 'EDIT_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const COMPLETE_ALL = 'COMPLETE_ALL'
export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'
Я почему то думаю, что нет и иногда встречая в чьём то коде
if (action.type === ADD_TODO) {
// ...
}
вместо ядрёного switch — case, я понимаю, что не единственный такой я на свете перфекционист, страдающий от этого "чуть-чуть не так как надо" в классическом Redux
Если Вам, уважаемый читатель, знакома эта боль, возрадуйтесь! под катом есть лекарство всего в две строчки кода :)
Особенно печально обстоят дела, когда Вы пытаетесь разобраться в крупном проекте со сложным UI. Вы видите, какие методы экшенов вызывает компонент, открываете файл с этими экшенами и хорошо, если там константы названы интуитивно, но бывают ситуации, когда Вы начинаете искать во всех редюсерах, куда же они прилетают.
По сути дела, dispatch — это метод Store, аналогичный по смыслу методу emit старого доброго EventEmitter и в терминах классической событийной модели, у нас фактически Store подписан на события, имена которых называются типами экшенов и которые принято задавать в виде вышеупомянутых констант, в связи с чем у меня постоянно возникал вопрос, почему я должен хранить это где то отдельно, да к тому же повторно прибегая к такому нелепому дублированию кода? Исходная мысль то ясна, нам необходимо подстраховаться от конфликтов и обеспечить некоторую консистентность между экшенами и редюсерами, но не уже ли нельзя сделать это как то элегантней?
Я понимаю, что люди разные и если у кого то возникнет аргументированное возражение на этот мой лёгкий дискомфорт от работы с кодом Redux, буду рад выслушать любые мнения в комментариях, но тем, кто разделяет сие чувство, позвольте представить redux-refine
Идея в основе проста:
Я предлагаю использовать вместо switch-case хэш, индексированный типом экшенов, так как в объекте не может быть одинаковых свойств, что исключает конфликты в рамках одного редьюсера, а так же позволяет экспортировать типы экшенов для модуля, из которого они диспатчатся
Так же, такой подход обеспечивает чистую связность кода, следующую логике one way binding и отражающему направление потока данных в приложении, а именно:
мы видим в компоненте, методы какого модуля с экшенами он использует, а в модуле с экшенами мы видим, каким редюсерам он отправляет экшены.
По просьбе tmnhy на наглядном примере поясню:
в экшенах мы делаем так:
import { actionTypes as types1 } from 'reducers/reducer1'
import { actionTypes as types2 } from 'reducers/reducer2'
const { ACTION_1_1, ACTION_1_2, ACTION_1_3 } = types1
const { ACTION_2_1, ACTION_2_2, ACTION_2_3 } = types2
в редюсерах так:
reducer1:
import { getActionTypes, connectReducers } from 'redux-refine'
export const initialState = {
value1: 0,
value2: '',
value3: null,
}
const reducers = {
ACTION_1_1: (state, {value1}) => ({...state, value1}),
ACTION_1_2: (state, {value2}) => ({...state, value2}),
ACTION_1_3: (state, {value3}) => ({...state, value3}),
}
export const actionTypes = getActionTypes(reducers)
export default connectReducers(initialState, reducers)
reducer2:
import { getActionTypes, connectReducers } from 'redux-refine'
export const initialState = {
value1: 0,
value2: '',
value3: null,
}
const reducers = {
ACTION_2_1: (state, {value1}) => ({...state, value1}),
ACTION_2_2: (state, {value2}) => ({...state, value2}),
ACTION_2_3: (state, {value3}) => ({...state, value3}),
}
export const actionTypes = getActionTypes(reducers)
export default connectReducers(initialState, reducers)
в том месте, где Вы предпочитаете комбинировать редюсеры всё по прежнему:
import { combineReducers } from 'redux'
import reducer1, { initialState as stateSection1 } from './reducer1'
import reducer2, { initialState as stateSection2 } from './reducer2'
export const intitialState = {
stateSection1, stateSection2
}
export default combineReducers({
stateSection1: reducer1,
stateSection2: reducer2
})
Да, конечно я понимаю, что это весьма мелочное нововведение, но мне от такого стиля работать с кодом на много приятней :)
И пожалуйста, не судите строго, если что — это мой первый пост на хабре
UPD:
Выхватив множество комментариев, интереснейших, но высказанных с разной долей недопонимания о том, что это вообще такое — redux-refine, я решил добавить ещё более детальное разъяснение:
Вот что я сделал:
1 Заменил конструкцию switch-case на выбор по ключу в хэшэ:
это
function reducer(state, {type, data}){
switch(type) {
case 'one': return {...state, ...data};
case 'two': return {...state, ...data};
case 'three': return {...state, ...data};
default: return state;
}
}
заменил на это
function reducer(state, {type, data}){
return ({
one: {...state, ...data},
two: {...state, ...data},
three: {...state, ...data}
})[type] || state;
}
2 Вынес хэш из скопа функции, что бы не пересоздавать его при каждом вызове, а каждое свойство этого хэша сделал методом чистой функцией, которой передаются необходимые данные в аргументах.
const reducers = {
one: (state, data) => ({...state, ...data}),
two: (state, data) => ({...state, ...data}),
three: (state, data) => ({...state, ...data})
}
function reducer(state, {type, data}){
return (redusers[type] || (state => state))(state, data);
}
3 Вместо объявления констант, использовал экспорт ключей хэша, так как они по определению уникальны, создав таким образом возможность импортировать эти ключи в экшенах из редьюсеров, что позволяет наглядно показать, с каким редьюсером работает данный модуль с экшенами.
И пожалуйста, не надо городить огород, так как redux-refine работает в точности так же, как и классический редьюсер — именно один в один, почему я и сказал, что можно его юзать одновременно с обычным подходом.
Это не какая то новая архитектура, не какой то там новый наворот — это просто улучшение кодестайла и обеспечение наглядности связей между модулями классического редакса.
Комментарии (37)
webdevium
21.11.2017 21:11+1Эх. И на кой эта статья? Неужели кто-то, понимающий в языке, не знает, что можно использовать словари?
elser Автор
21.11.2017 22:33Поймите правильно, я не сомневаюсь в профессионализме кого-то понимающего, но всё равно ведь конкретно в этом случае практически везде используются подходы
компонент -> экшены < — константы -> редьюсеры
или
компонент -> экшены < — редьюсеры
вместо логически правильного
компонент -> экшены -> редьюсеры
где стрелкой показано, откуда что импортируетсяwebdevium
21.11.2017 22:37Понимаю правильно, естественно.
Но проблема более фундаментальная — слепое наследование кода, который начинающие разработчики копируют у таких же начинающих разработчиков, которые прочли 2-3 страницы из книги и бегут писать статьи с громкими криками «так правильно. просто выключайте мозги. копируйте этот код».
flancer
22.11.2017 08:44Действительно, а на кой нужна азбука? Неужели читающий слова не знает букв?!
Кстати, дискуссия под этой "ненужной" статье вполне себе годная. Может для этого?
keenondrums
21.11.2017 21:12+1По-большому счету вы изобрели заново redux-actions, с той лишь разницей, то намертво связали имена констант с их значениями. Это плохо потому, что для констант обычно используются довольно короткие имена. Есть очень не маленькая вероятность, то при таком походе при подклюении очередной библиотеки ваши типы экшнов совпадут, и приехали. Решение было предложено в весьма популярном пропосале ducks-modular-redux
elser Автор
21.11.2017 21:32Ну, вообще то в классической концепции, разные редюсеры теоретически могут реагировать на один и тот же экшн, обновляя по нему разные участки стора. Если взглянуть на то, как это выглядит на уровне combineReducers, то в этом нет ничего страшного:
// псевдо код для пояснения: combineReducers({ storeSection1: reducer1, storeSection2: reducer2 }); // классический вариант: // reducer1 // здесь state - это storeSection1 function reducer1(state, {type, data}){ switch(type){ case SOMETHING_GLOBAL_HAPPENS: return {...state, newValue: data.newValue}; case SOMETHING_FOR_REDUCER1: return {...state, newValue: data.newValue}; } } // reducer2 // здесь state - это storeSection2 function reducer1(state, {type, data}){ switch(type){ case SOMETHING_GLOBAL_HAPPENS: return {...state, newValue: data.newValue}; case SOMETHING_FOR_REDUCER2: return {...state, newValue: data.newValue}; } } // В моём варианте в точности то же самое, но в другом немного codestyle: // reducer1 reducer = { SOMETHING_GLOBAL_HAPPENS: (state, {newValue}) => { ...state, newValue }, SOMETHING_FOR_REDUCER1: (state, {newValue}) => { ...state, newValue } } // reducer2 reducer = { SOMETHING_GLOBAL_HAPPENS: (state, {newValue}) => { ...state, newValue }, SOMETHING_FOR_REDUCER2: (state, {newValue}) => { ...state, newValue } }
keenondrums
21.11.2017 23:10Это будет работать нормально ровно до тех ор, пока вы контролируете все эти редьюсеры и их логику. А когда используется сторонняя библиотека, то могу быть болшие проблемы. Развязывание экшн типов и констант им соотвествующих — это благо.
// your reducer const initialState = { num: 1 } TYPE1: ( { num } ) => ({ num: num + 1 }) // third-party reducer which source code you can not affect TYPE1: (state) => throw new Error('Blow up the world')
Еще веселее станет дебажить, если над проектом работает несколько человек, и вы назвали свои экшн типы одниаковоelser Автор
22.11.2017 00:32Давайте ещё раз проясним: разные редьюсеры аффектят разные разделы стора — так устроен combineReducers
Один редьюсер может использовать redux-refine и тогда модули с экшенами должны импортировать типы экшенов из этого редьюсера, при этом одновременно другой редьюсер может использовать классический стиль и тогда Вам необходимо будет импортировать константы, там, где Вы диспатчите эти экшены
Одно другому не мешает, а throw Error() из редьюсера свалит апликуху в любом случае, так что не мутите воду, а поймите суть
elser Автор
21.11.2017 21:37Поддерживаю и даже скажу больше, baobab — тоже вещь! Но корпорейт неумолим и во многих проектах всё таки redux
debounce
21.11.2017 22:21Только подсел на функциональщину, react и redux.
Подскажите по архитектуре, пожалуйста.
Переписываю приложение рисовалку — это мегатонны событий mousemove.
В redux подходе на каждое такое событие нужно генерировать action, а в редюсере возвращать иммутабельный стейт.
Правильно ли так использовать redux, когда не будет красивых логов ADD_TODO, EDIT_TODO и таймтревела, а будут километры одних только MOUSE_MOVE?
webdevium
21.11.2017 22:42Твой ник тебе в помощь. Или throttle.
Каждый MOUSE_MOVE будет складировать в очередь свое действие, к примеру, точки движения или цвета.
И только каждый N-й MOUSE_MOVE будет вызывать полное действие, типа записи в пользовательскую историю движений, как это примерно выглядит в photoshop.TheShock
22.11.2017 18:08Твой ник тебе в помощь. Или throttle
Как вы себе представляете debounce в рисовалке? Вот я провел мышкой, нарисовал какую-нибудь линию кривую. И за счет дебаунса вместо кривой она станет прямой — вместо всех точек mousemove будет только первая и последняя.
В Фотошоп записывается каждое движение мыши, естественно, а не так, как вы говорите. Просто при mouseup все эти движения отмечаются как единое действие.
elser Автор
21.11.2017 22:43В
Store
приложения Вы храните данные приложения, преимущественно относящиеся к бизнес логике. Разумеется, Вы можете использоватьStore
для хранения вообще всего чего угодно, но это не означает, что Вы совсем не должны использовать внутренний state компонентов.
Просто следуйте простым правилам: если это простой переносимый компонент, не реализующий бизнес логику конкретного приложения, то он подучает данные через props и возвращает их в аргументах обработчиков событий, при этом не брезгуя хранением каких либо значений, использующихся только для собственных нужд, в
this.state
Ярким примером такого поведения могут служить controlled components
debounce
21.11.2017 23:01в том-то и дело, что это и есть основной функционал, все остальное будет прикручено react'ом, а вот что делать с событиями мыши?
мой главный стейт выглядит примерно так
{ "nodes": [{ "id": 1, "x": 390, "y": 99 }, { "id": 2, "x": 687, "y": 85 }, { "id": 3, "x": 738, "y": 321 }], "edges": [{ "id": 1, "from": 1, "to": 2 }, { "id": 2, "from": 2, "to": 3 }, { "id": 3, "from": 3, "to": 4 }], }
это граф, а само приложение — рисовалка помещений
elser Автор
21.11.2017 23:37Так у Вас же не постоянно движение мыши мониторятся, а в момент выполнения какого то действия, так? на пример при перемещении ноды, или ребра.
У Вас основная нагрузка приходится не на обновление стора, а на перерисовку UI при его обновлении, так как на пример при перемещении узла, со стороны стора у Вас меняется только объект с данными соответствующего узла, ссылка на массив узлов обновляется, так как в редюсере формируется новый массив (что да, при большом количестве узлов может быть немного тяжеловатым, но это надо ооочень много элементов, так как все, кроме изменяемого — просто копирование ссылок на объекты)
И меняется сам стор (NextStore !== Store), но те ветки стора, которых изменения не коснулись просто переносятся в новое состояние как есть.
Далее летит update компонентам и вот тут надо всё разрулить правильно, либо контролируя кто должен перерисоваться, при помощи shouldComponentUpdate, либо используя reselect, для тех же целей (подробнее тут) Но поверьте, самому стору такой поток событий не особо в тягость, особенно, если принять во внимание совет webdevium и всандалить на этом потоке лёгкий троттлинг на малых таймингах
faiwer
22.11.2017 07:26А где именно вы формируете action-"логи"? Если руками, то поддерживайте склейку определённых action-ов (например по маске для action.type). Если в redux-devtools — то так ли сильно оно вам надо?
А вообще я обычно все подобные mouseHover вещи реализовываю через setState или ещё более обходными путями. Не в последнюю очередь из-за того, что react-redux будет вызывать все свои callback-и для connect-утых компонентов на каждый тик вашего mouseHover. Это, в случае большого приложения, легко может привести к тормозам.
Вообще говоря, чем дальше в лес, тем больше кода я пишу в обход redux, оставляя там только то, что имеет прямое отношение к бизнес-логике (99% кода), а не к сию-миллисекундному интерактиву, анимациям (не всё удаётся сделать через css).
comerc
21.11.2017 23:20Погуглите ducks-pattern + redux-act. Никаких констант. Экшены с редюсерами живут в одном файле. И тестить это проще. Я опубликовал заметку недавно.
elser Автор
22.11.2017 00:13а что касается redux-act, то там тоже всё наоборот. Разъясню на пальцах — то, что отправляет события определяет их имена и как бы рассказывает стору: «я что то там диспатчу, значит ты должен быть на это подписан», создавая зависимость, направленную в сторону, обратную data flow
Правильней так: «Стор подписан на определённые действия, отправка которых должна быть имплементирована в экшенах»
В итоге мы получаем разработку, в которой мы работаем с состоянием приложения, не зависимо от того, что его меняет, так как инициация изменений может происходить в куче разных мест, а прилетают они в единое, четко определённое место и я могу поспорить на счет удобства тестирования — redux-refine тестируется на много лучше и тесты могут быть сколь угодно атомарными
faiwer
22.11.2017 07:46Я пришёл к следующей схеме:
Файл рядового модуля:const PRE = 'уникальный_префикс'; const OPEN = `${PRE}_OPEN`; export const open = id => ({ type: OPEN, id }); export const map = { [OPEN]: (st, { id }) => ({ ...st, id, active: true }), };
wheercool
22.11.2017 11:24Redux был построен на идее Free Monad. Если перевести на ООП терминологию то это паттерн Interpretator. Actions — это DSL, а reducers — это Интерпретатор. Основной смысли и преимущество в том, что уровень Actions(DSL) изолирован от того, как он должен интерпретироваться и в любой момент можно для него написать другой интерпретатор (более эффективный, либо с измененной логикой).
Вы же в Actions делаете связь с reducer, т.е. ваша абстракция начинает зависеть от реализации.
Я не говорю, что ваше решение плохое, но это уже не redux. Возникает тогда логичный вопрос, а зачем вам вообще тогда Actions?
Что такое store — это контейнер состояния. Основываясь на том как вы его используете, могу предложить вам перейти просто на классы.
class Store { action1() { } action2() { } }
elser Автор
22.11.2017 21:39как то вы усложняете ) Всё гораздо проще. Вот что я сделал:
Заменил конструкцию switch-case на выбор по ключу в хэшэ
пояснение:
function choice(selector){ switch(selector) { case 'one': return 1; case 'two': return 2; case 'three': return 3; default: return 4; } }
заменил на
function choice(selector) { return ({ one: 1, two: 2, three: 3 })[selector] || 4; }
Затем вынес хэш из скопа функции, что бы не пересоздавать его при каждом вызове, а каждое свойство этого хэша сделал методом, которому передаются необходимые данные в аргументах.
Затем просто вместо объявления констант заюзал экспорт ключей хэша, так как они по определению уникальны, создав таким образом возможность импортировать эти ключи в экшенах из редьюсеров, что позволяет наглядно показать, с каким редьюсером работает данный модуль с экшенами.
И не надо городить огород, так как redux-refine работает в точности так же, как и классический редьюсер — именно один в один, почему я и сказал, что можно его юзать одновременно с обычным подходом.
Это не какая то новая архитектура, не какой то там новый наворот — это просто улучшение кодестайла и наглядности связей между модулями классического редакса.
hubhito
22.11.2017 16:33Покритикуете такой подход?
// actions.js import { createAction } from 'redux-actions' export const someAction = createAction('SOME_ACTION') // reducers.js import { handleAction } from 'redux-actions' import { Map } from 'immutable' import * as actions from './actions' const someAction = (state, action) => { const { value } = action.payload return state.set('value', value) } export default [ handleAction(actions.someAction().type, someAction, Map()), // ... other reducers ] // rootReducer.js import reducers from './reducers' const rootReducer = (state, action) => reducers.reduce((state, reducer) => reducer(state, action), state)
tmnhy
И где конкретика, примеры кода?
elser Автор
В репозитории по ссылке на проект есть, но видимо действительно надо было сдублировать в статью. Просто идея слишком проста, как далее и пишет webdevium и я решил её не перегружать
tmnhy
Без примеров неполноценно, а лезть из статьи на сторонний ресурс, чтобы понять что да как, совсем плохо. В принципе, тогда после преамбулы можно было сразу ссылку на репозиторий дать и всё.
elser Автор
Добавил пояснение, спасибо за подсказку