Как стать автором
Обновить

Комментарии 18

Cогласно предостережению Дэна, мы только проигрываем в потере дополнительной информации в логгере и дев-тузлах.

Не буду говорить за остальных, но лично в моем флоу дебага это crucial

Мне кажется, нет смысла пытаться натянуть на redux ООП — если библиотека задумана в функциональном стиле, то надо ему и следовать. Из опыта (в js) попытки «натянуть» функциональное на императивное скорее всего приведут к сложностям с поддержкой и снизят скорость разработки. Если ООП ближе, стоит посмотреть, к примеру, на mobX.
По-моему, сочетание двух парадигм это не плохо. Здесь от ООП мы только используем класс для упорядочивания методов, а не JS-модуль.

А чем вас не устраивает стандартный способ упорядочиваения методов с import/export?
Можно положить экшены и редюсер в один файл, такой подход называется Ducks.


export const actionA () => {/*action code*/}

export const actionB () => {/*action code*/}

export default function reducer(state, action) {
 // reducer code
}

использование:


// если нужен редьюсер
import reducer from './ducks';

// если нужны экшены
import {actionA, actionB} from './ducks';
Извиняюсь, выше я выразился не совсем корректно, поэтому у вас возникло неправильное понимание. Класс используется не только для организации методов, но и для того, чтобы этот подход вообще работал. Модуль (ECMAScript 2015) — это статическая единица кода, а нам нужны новые экземпляры редьюсеров и генераторов экшенов с уникальными типами для экшенов.

Можно завернуть символы в фабрику:


function createList() {
  const ACTION_A = Symbol();
  const ACTION_В = Symbol();
  return {
    actionA: (payload) => ({type: ACTION_A, payload}),
    actionB: (payload) => ({type: ACTION_B, payload}),
    reducer: (state, action) => {
      // reducer code
    }
  };
}

Большой разницы между createList() и new List() нет, зато не нужно делать никаких оговорок типа "Генераторы экшенов и редьюсер должны быть методами экземпляра класса", как у вас в статье.

Ну, тут дело вкуса. Если вам нравится фабрика — пожалуйста. Мне класс (хотя он тоже является функцией) кажется более изящным.
Не совсем напрямую, несколько иначе обходим эту проблему.
Стараемся сделать так, чтобы reducer's можно было экспортировать / импортировать
как обычную «библиотеку».

coffeescript
    ### ------------------------------------------------ actions.coffee ###

    { createAction } = require 'redux-actions'
    ###
    DATA API
    ###
    module.exports =
      # PUBLIC
      create: createAction 'CREATE_MESSAGE', (body, header) ->
        action =
          body: body
          header: header
        action
      remove: createAction 'REMOVED_MESSAGE', (id) ->
        id: id


      # Private
      $:
        creating: createAction 'CREATING_MESSAGE', (message) ->
          _.assign {}, message,
            id: _.uniqueId('message-')
            synced: false
        created: createAction 'CREATED_MESSAGE'

    ### ------------------------------------------------ reducer.coffee ###


    _ = require 'lodash'
    { handleActions } = require 'redux-actions'


    ###
    STATE CHANGES
    ###
    actions = require './actions'

    actionsMap = [
        action: actions.$.creating
        handle: (state, action) ->
          _.concat state, _.assign {}, action.payload
      ,
        action: actions.$.created
        handle: (state, action) ->
          _.map _.cloneDeep(state), (item) ->
            if item.id is action.payload
              item.synced = true
            item
      ,
        action: actions.remove
        handle: (state, action) ->
          _.reject _.cloneDeep(state), action.payload
    ]
    module.exports =
      messages: handleActions _.mapValues(_.keyBy(actionsMap, 'action'), 'handle'), []

  


JS
    /* ------------------------------------------------ actions.coffee */
    var _, actions, actionsMap, createAction, handleActions;

    createAction = require('redux-actions').createAction;


    /*
        DATA API
    */

    module.exports = {
      create: createAction('CREATE_MESSAGE', function(body, header) {
        var action;
        action = {
          body: body,
          header: header
        };
        return action;
      }),
      remove: createAction('REMOVED_MESSAGE', function(id) {
        return {
          id: id
        };
      }),
      $: {
        creating: createAction('CREATING_MESSAGE', function(message) {
          return _.assign({}, message, {
            id: _.uniqueId('message-'),
            synced: false
          });
        }),
        created: createAction('CREATED_MESSAGE')
      }
    };


    /* ------------------------------------------------ reducer.coffee */

    _ = require('lodash');

    handleActions = require('redux-actions').handleActions;


    /*
        STATE CHANGES
     */

    actions = require('./actions');

    actionsMap = [
      {
        action: actions.$.creating,
        handle: function(state, action) {
          return _.concat(state, _.assign({}, action.payload));
        }
      }, {
        action: actions.$.created,
        handle: function(state, action) {
          return _.map(_.cloneDeep(state), function(item) {
            if (item.id === action.payload) {
              item.synced = true;
            }
            return item;
          });
        }
      }, {
        action: actions.remove,
        handle: function(state, action) {
          return _.reject(_.cloneDeep(state), action.payload);
        }
      }
    ];

    module.exports = {
      messages: handleActions(_.mapValues(_.keyBy(actionsMap, 'action'), 'handle'), [])
    };

  

Прочитал с листа код из примера, а из статьи смог вынести полезного про уникальность возвращаемых значений символов.


Поставьте себя на место тех, кто будет на поддержке кода. Пожалуйста!

а я тут эксперементирую на досуге с разделением управления данными, бизнес логики и отображением: https://www.npmjs.com/package/react-component-redux

Ох, вообще печально это всё.
Поэтому не надо использовать строки в качестве типов экшнов. Нужно задать константы с некоим подобием неймспейса в файле с редьюсером. Так делается в иерархическом redux, т.н. ducks
const INCREMENT = "counter1/INCREMENT";
const DECREMENT= "counter1/DECREMENT";
...
function counter(state = 0, action) {
    switch (action.type) {
        case INCREMENT:
            return state + 1;
        case DECREMENT:
            return state - 1;
        default:
            return state;
    }
}

Так мы еще сможем сохранить информацию для дев-тулз, где четко будет видно какой именно экшн сработал.

Красивый способ избавиться от switch:


import { createAction } from 'redux-act'

export const inputTitle = createAction('@@edit_post/INPUT_TITLE', title => ({ title }))
export const submit = createAction('@@edit_post/SUBMIT', post => post)

import { createReducer } from 'redux-act'
import * as EditPostActions from '../actions/EditPostActions'

const initialState = {
  flow: '',
  title: '',
  content: ''
}

const reducer = createReducer({
  [EditPostActions.inputTitle]: (state, { title }) => ({...state, title}),
  [EditPostActions.submit]: (state, post) => ({...state, ...post})
}, initialState)

export default reducer

Спасибо за ducks, теперь вообще замечательно!


import { createAction, createReducer } from 'redux-act'

export const actions = {
  inputTitle: createAction('@@edit_post/INPUT_TITLE', title => ({ title })),
  submit: createAction('@@edit_post/SUBMIT', post => post),
}

const initialState = {
  flow: '',
  title: '',
  content: ''
}

const reducer = createReducer({
  [actions.inputTitle]: (state, { title }) => ({...state, title}),
  [actions.submit]: (state, post) => ({...state, ...post})
}, initialState)

export default reducer
Не очень удачно назвал статью изначально. Больше интересует организация Redux-модуля для переиспользования, без дублирования редьюсеров и генераторов экшенов.
Те, кому важны девтулзы, добавил реализацию без этих ограничений.
List.devtools.ready.js

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


Пример c counter
const COUNTER_INCREMENT = "counter increment"
const COUNTER_DECREMENT = "counter decrement"
const COUNTER_INIT = "counter init"

const getInitialCounter = ({
    counter: 0
})

const counter(initialState = {}, action) {
    const {payload, type} = action;

    switch(type) {
        case COUNTER_INIT: {
            const {name} = payload

            return {
                ...state,
                [name]: getInitialCounter()
            }
        },
        case COUNTER_INCREMENT: {
            const {name} = payload
            const oldCounter = state[name]
            const updatedCounter = {
                counter: oldCounter.counter++
            }

            return {
                ...state,
                [name]: updatedCounter
            }
        }
        case COUNTER_DECREMENT: {
            const {name} = payload
            const oldCounter = state[name]
            const updatedCounter = {
                counter: oldCounter.counter--
            }

            return {
                ...state,
                [name]: updatedCounter
            }
        }
        default return state;
    }
}

const incrementCounter(name) => ({type: COUNTER_INCREMENT, name})
const decrementCounter(name) => ({type: COUNTER_DECREMENT, name})
const initCounter = (name) => ({type: COUNTER_INIT, name})

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации