redux-refine — простая радость перфекциониста +9


image


Скажите, люди, я один испытываю небольшой душевный зуд
от необходимости писать нечто вот эдакое? :


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 работает в точности так же, как и классический редьюсер — именно один в один, почему я и сказал, что можно его юзать одновременно с обычным подходом.


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




К сожалению, не доступен сервер mySQL