[Из песочницы] Redux-symbiote — пишем действия и редьюсеры почти без боли

habr.png

React-redux замечательная штука. При правильном использовании архитектура приложения эффективна, а структура проекта и легко читаемая. Но как и в любом решении есть свои особенности.

Описание действий и редьюсеров одна из таких особенностей. Классическая реализация двух этих сущностей в коде довольно трудоемкое занятие.

Боль классической реализации


Простой пример:

// actionTypes.js
// описываем типы действий
export const POPUP_OPEN_START = 'POPUP_OPEN_START ';
export const POPUP_OPEN_PENDING = 'POPUP_OPEN_PENDING ';
export const POPUP_OPEN_SUCCESS = 'POPUP_OPEN_SUCCESS ';
export const POPUP_OPEN_FAIL = 'POPUP_OPEN_FAIL';

export const POPUP_CLOSE_START = 'POPUP_CLOSE_START ';
export const POPUP_CLOSE_PENDING = 'POPUP_CLOSE_PENDING ';
export const POPUP_CLOSE_SUCCESS = 'POPUP_CLOSE_SUCCESS ';
export const POPUP_CLOSE_FAIL = 'POPUP_CLOSE_FAIL';

// actions.js
// описываем сами действия

import {
  POPUP_OPEN_START,
  POPUP_OPEN_PENDING,
  POPUP_OPEN_SUCCESS,
  POPUP_OPEN_FAIL,
  POPUP_CLOSE_START,
  POPUP_CLOSE_PENDING,
  POPUP_CLOSE_SUCCESS,
  POPUP_CLOSE_FAIL
} from './actionTypes';

export function popupOpenStart(name) {
  return {
    type: POPUP_OPEN_START,
    payload: {
      name
    },
  }
}

export function popupOpenPending(name) {
  return {
    type: POPUP_OPEN_PENDING,
    payload: {
      name
    },
  }
}

export function popupOpenFail(error) {
  return {
    type: POPUP_OPEN_FAIL,
    payload: {
      error,
    },
  }
}

export function popupOpenSuccess(name, data) {
  return {
    type: POPUP_OPEN_SUCCESS,
    payload: {
      name,
      data
    },
  }
}

export function popupCloseStart(name) {
  return {
    type: POPUP_CLOSE_START,
    payload: {
      name
    },
  }
}

export function popupClosePending(name) {
  return {
    type: POPUP_CLOSE_PENDING,
    payload: {
      name
    },
  }
}

export function popupCloseFail(error) {
  return {
    type: POPUP_CLOSE_FAIL,
    payload: {
      error,
    },
  }
}

export function popupCloseSuccess(name) {
  return {
    type: POPUP_CLOSE_SUCCESS,
    payload: {
      name
    },
  }
}

// reducers.js
// реализуем редьюсеры

import {
  POPUP_OPEN_START,
  POPUP_OPEN_PENDING,
  POPUP_OPEN_SUCCESS,
  POPUP_OPEN_FAIL,
  POPUP_CLOSE_START,
  POPUP_CLOSE_PENDING,
  POPUP_CLOSE_SUCCESS,
  POPUP_CLOSE_FAIL
} from './actionTypes';

const initialState = {
  opened: []
};

export function popupReducer(state = initialState, action) {
  switch (action.type) {
    case POPUP_OPEN_START:
    case POPUP_OPEN_PENDING:
    case POPUP_CLOSE_START:
    case POPUP_CLOSE_PENDING:
      return {
        ...state,
        error: null,
        loading: true
      };

    case POPUP_OPEN_SUCCESS :
       return {
        ...state,
        loading: false,
        opened: [
          ...(state.opened || []).filter(x => x.name !== action.payload.name),
          {
             ...action.payload
          }
        ]
      };
    case POPUP_OPEN_FAIL:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
    
    case POPUP_CLOSE_SUCCESS:
       return {
        ...state,
        loading: false,
        opened: [
            ...state.opened.filter(x => x.name !== name)
        ]
      };
    case POPUP_CLOSE_FAIL:
      return {
        ...state,
        loading: false,
        error: action.payload.error
      };
  }

  return state;
}


На выходе имеем 3 файла и как минимум следующие проблемы:

  • «раздувание» кода при простом добавлении новой цепочки действий
  • избыточный импорт констант действий
  • чтение имен констант действий (индивидуально)


Оптимизация


Данный пример можно улучшить с помощью redux-actions.

import { createActions, handleActions, combineActions } from 'redux-actions'

export const actions = createActions({
    popups: {
        open: {
            start: () => ({ loading: true }),
            pending: () => ({ loading: true }),
            fail: (error) => ({ loading: false, error }),
            success: (name, data) => ({ loading: false, name, data }),
        },
        close: {
            start: () => ({ loading: true }),
            pending: () => ({ loading: true }),
            fail: (error) => ({ loading: false, error }),
            success: (name) => ({ loading: false, name }),
        },
    },
}).popups

const initialState = {
    opened: []
};

export const accountsReducer = handleActions({
    [
        combineActions(
            actions.open.start,
            actions.open.pending,
            actions.open.success,
            actions.open.fail,
            actions.close.start,
            actions.close.pending,
            actions.close.success,
            actions.close.fail
        )
    ]: (state, { payload: { loading } }) => ({ ...state, loading }),

    [combineActions(actions.open.fail, actions.close.fail)]: (state, { payload: { error } }) => ({ ...state, error }),

    [actions.open.success]: (state, { payload: { name, data } }) => ({
        ...state,
        error: null,
        opened:
        [
            ...(state.opened || []).filter(x => x.name !== name),
            {
                name, data
            }
        ]
    }),

    [actions.close.success]: (state, { payload: { name } }) => ({
        ...state,
        error: null,
        opened:
        [
            ...state.opened.filter(x => x.name !== name)
        ]
    })
}, initialState)


Уже намного лучше, но совершенству нет предела.

Лечим боль


В поисках более оптимального решения наткнулись на комментарий LestaD habr.com/ru/post/350850/#comment_10706454 и решили попробовать redux-symbiote.
Это позволило убрать лишние сущности и уменьшить количество кода.

Пример выше стал выглядеть вот так:

// symbiotes/popups.js

import { createSymbiote } from 'redux-symbiote';

export const initState = {
  opened: []
};

export const { actions, reducer } = createSymbiote(initialState, {
  popups: {
    open: {
      start: state => ({ ...state, error: null }),
      pending: state => ({ ...state }),
      success: (state, { name, data } = {}) => ({
        ...state,
        opened: [
            ...(state.opened || []).filter(x => x.name !== name),
            {
              name,
              data
            })
        ]
      }),
      fail: (state, { error } = {}) => ({ ...state, error })
    },
    close: {
      start: state => ({ ...state, error: null }),
      pending: state => ({ ...state }),
      success: (state, { name } = {}) => ({
        ...state,
        opened: [
          ...state.opened.filter(x => x.name !== name)
        ]
      }),
      fail: (state, { error } = {}) => ({ ...state, error })
    }
  }
});


// пример вызова

import {
  actions
} from './symbiotes/popups';

// ...

export default connect(
  mapStateToProps,
  dispatch => ({
    onClick: () => {
        dispatch(actions.open.start({ name: PopupNames.Info }));
    }
  })
)(FooComponent);


Из плюсов имеем:

  • все в одном файле
  • меньше кода
  • структурированное представление действий


Из минусов:

  • IDE не всегда предлагает подсказки
  • сложно искать действие в коде
  • сложно переименовать действие


Не смотря на минусы данный модуль успешно используется в наших проектах.

Спасибо LestaD за хорошую работу.

© Habrahabr.ru