Доступ к состоянию Redux в создателе действий?

Скажи, что у меня есть следующее:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
  }
}

И в этом создателе действий я хочу получить доступ к глобальному состоянию хранилища (все редукторы). Это лучше сделать это:

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

или это:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return (dispatch, getState) => {
    const {items} = getState().otherReducer;

    dispatch(anotherAction(items));
  }
}

4 ответа

Существуют разные мнения о том, является ли доступ к состоянию в действии создателями хорошей идеей. В тех немногих случаях использования, которые я считаю приемлемыми, можно проверить кэшированные данные перед выполнением запроса или проверить, прошли ли вы проверку подлинности (другими словами, выполнить условную диспетчеризацию). Я думаю, что передача данных, таких как state.something.items в действии создатель определенно является анти-паттерном и не рекомендуется, потому что он затеняет историю изменений: если есть ошибка и items неверны, трудно отследить, откуда эти неправильные значения, потому что они уже являются частью действия, а не вычисляются напрямую редуктором в ответ на действие. Так что делай это с осторожностью. (Дальнейшее обсуждение плюсов и минусов доступа к состоянию в создателях действий см. В блоге Idiomatic Redux: мысли о Thunks, Sagas, Abstraction и Reusability.)

Если вы обнаружите, что вам это нужно, оба предложенных вами подхода подойдут. Первый подход не требует промежуточного программного обеспечения:

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

Однако вы можете видеть, что это зависит от store будучи синглтоном, экспортированным из некоторого модуля. Мы не рекомендуем этого, потому что это значительно усложняет добавление рендеринга сервера в ваше приложение, потому что в большинстве случаев на сервере вам понадобится отдельное хранилище для каждого запроса. Поэтому, хотя технически этот подход работает, мы не рекомендуем экспортировать магазин из модуля.

Вот почему мы рекомендуем второй подход:

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return (dispatch, getState) => {
    const {items} = getState().otherReducer;

    dispatch(anotherAction(items));
  }
}

Для этого потребуется использовать промежуточное ПО Redux Thunk, но оно отлично работает как на клиенте, так и на сервере. Вы можете прочитать больше о Redux Thunk и почему это необходимо в этом случае здесь.

В идеале ваши действия не должны быть "жирными" и содержать как можно меньше информации, но вы должны свободно делать то, что лучше всего подходит для вас в вашем собственном приложении. В Redux FAQ есть информация о том, как разделить логику между создателями действий и редукторами, и о том, когда это может быть полезно для использования. getState в действии создателя.

Когда ваш сценарий прост, вы можете использовать

import store from '../store';

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
  return {
    type: SOME_ACTION,
    items: store.getState().otherReducer.items,
  }
}

Но иногда ваш action creator нужно вызвать несколько действий

например асинхронный запрос, так что вам нужно REQUEST_LOADREQUEST_LOAD_SUCCESSREQUEST_LOAD_FAIL действия

export const [REQUEST_LOAD, REQUEST_LOAD_SUCCESS, REQUEST_LOAD_FAIL] = [`REQUEST_LOAD`
    `REQUEST_LOAD_SUCCESS`
    `REQUEST_LOAD_FAIL`
]
export function someAction() {
    return (dispatch, getState) => {
        const {
            items
        } = getState().otherReducer;
        dispatch({
            type: REQUEST_LOAD,
            loading: true
        });
        $.ajax('url', {
            success: (data) => {
                dispatch({
                    type: REQUEST_LOAD_SUCCESS,
                    loading: false,
                    data: data
                });
            },
            error: (error) => {
                dispatch({
                    type: REQUEST_LOAD_FAIL,
                    loading: false,
                    error: error
                });
            }
        })
    }
}

Примечание: вам нужен redux-thunk, чтобы вернуть функцию в создатель действия

Я согласен с @Bloomca. Передача нужного значения из хранилища в диспетчерскую функцию в качестве аргумента выглядит проще, чем экспорт хранилища. Я сделал пример здесь:

import React from "react";
import {connect} from "react-redux";
import * as actions from '../actions';

class App extends React.Component {

  handleClick(){
    const data = this.props.someStateObject.data;
    this.props.someDispatchFunction(data);
  }

  render(){
    return (
      <div>       
      <div onClick={ this.handleClick.bind(this)}>Click Me!</div>      
      </div>
    );
  }
}


const mapStateToProps = (state) => {
  return { someStateObject: state.someStateObject };
};

const mapDispatchToProps = (dispatch) => {
  return {
    someDispatchFunction:(data) => { dispatch(actions.someDispatchFunction(data))},

  };
}


export default connect(mapStateToProps, mapDispatchToProps)(App);

Я хотел бы отметить, что читать из магазина не так уж и плохо - может быть, гораздо удобнее решить, что делать на основе хранилища, чем передать все компоненту, а затем в качестве параметра функция. Я полностью согласен с Дэном в том, что гораздо лучше не использовать хранилище в качестве синглтона, если только вы не уверены на 100%, что будете использовать его только для рендеринга на стороне клиента (в противном случае могут возникать трудные для отслеживания ошибки).

Недавно я создал библиотеку, чтобы справляться с многословностью приставок, и я думаю, что было бы неплохо поместить все в промежуточное ПО, чтобы все было как инъекция зависимостей.

Итак, ваш пример будет выглядеть так:

import { createSyncTile } from 'redux-tiles';

const someTile = createSyncTile({
  type: ['some', 'tile'],
  fn: ({ params, selectors, getState }) => {
    return {
      data: params.data,
      items: selectors.another.tile(getState())
    };
  },
});

Однако, как вы можете видеть, мы на самом деле не изменяем данные здесь, поэтому есть большая вероятность, что мы можем просто использовать этот селектор в другом месте, чтобы объединить его где-то еще.

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

Вы можете получить состояние от редукторов к действиям, разделив действие на две отдельные функции: сначала запросите данные, затем выполните действие с данными. Вы можете сделать это с помощью redux-loop,

Сначала "пожалуйста, попросите данные"

export const SOME_ACTION = 'SOME_ACTION';
export function someAction() {
    return {
        type: SOME_ACTION,
    }
}

В редукторе перехватите запрос и предоставьте данные для действия второго этапа, используя redux-loop,

import { loop, Cmd } from 'redux-loop';
const initialState = { data: '' }
export default (state=initialState, action) => {
    switch(action.type) {
        case SOME_ACTION: {
            return loop(state, Cmd.action(anotherAction(state.data))
        }
    }
}

С данными в руках, делайте, что вы изначально хотели

export const ANOTHER_ACTION = 'ANOTHER_ACTION';
export function anotherAction(data) {
    return {
        type: ANOTHER_ACTION,
        payload: data,
    }
}

Надеюсь, это кому-нибудь поможет.

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

Именно здесь селектор имеет для меня наибольший смысл. Компоненту, который выдает этот запрос, следует сообщить, что пора его выдать путем выбора.

export const SOME_ACTION = 'SOME_ACTION';
export function someAction(items) {
  return (dispatch) => {
    dispatch(anotherAction(items));
  }
}

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

Я хотел бы предложить еще один вариант, который я считаю наиболее чистым, но он требует react-redux или что-то похожее - также я использую несколько других необычных функций на этом пути:

// actions.js
export const someAction = (items) => ({
    type: 'SOME_ACTION',
    payload: {items},
});
// Component.jsx
import {connect} from "react-redux";

const Component = ({boundSomeAction}) => (<div
    onClick={boundSomeAction}
/>);

const mapState = ({otherReducer: {items}}) => ({
    items,
});

const mapDispatch = (dispatch) => bindActionCreators({
    someAction,
}, dispatch);

const mergeProps = (mappedState, mappedDispatches) => {
    // you can only use what gets returned here, so you dont have access to `items` and 
    // `someAction` anymore
    return {
        boundSomeAction: () => mappedDispatches.someAction(mappedState.items),
    }
});

export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);
// (with  other mapped state or dispatches) Component.jsx
import {connect} from "react-redux";

const Component = ({boundSomeAction, otherAction, otherMappedState}) => (<div
    onClick={boundSomeAction}
    onSomeOtherEvent={otherAction}
>
    {JSON.stringify(otherMappedState)}
</div>);

const mapState = ({otherReducer: {items}, otherMappedState}) => ({
    items,
    otherMappedState,
});

const mapDispatch = (dispatch) => bindActionCreators({
    someAction,
    otherAction,
}, dispatch);

const mergeProps = (mappedState, mappedDispatches) => {
    const {items, ...remainingMappedState} = mappedState;
    const {someAction, ...remainingMappedDispatch} = mappedDispatch;
    // you can only use what gets returned here, so you dont have access to `items` and 
    // `someAction` anymore
    return {
        boundSomeAction: () => someAction(items),
        ...remainingMappedState,
        ...remainingMappedDispatch,
    }
});

export const ConnectedComponent = connect(mapState, mapDispatch, mergeProps)(Component);

Если вы хотите повторно использовать это, вам нужно будет извлечь конкретный mapState, mapDispatch а также mergeProps в функции для повторного использования в другом месте, но это делает зависимости совершенно ясными.

Я бы не получил доступ к состоянию в Action Creator. Я бы использовал mapStateToProps() и импортировал бы весь объект состояния и импортировал бы файл CombinedReducer (или import * from './reducers';) в компоненте, в который в конечном итоге собирается Action Creator. Затем используйте деструктуризацию в компоненте, чтобы использовать все, что вам нужно из свойства состояния. Если Action Creator передает состояние в Редуктор для данного ТИПА, вам не нужно упоминать состояние, потому что редуктор имеет доступ ко всему, что в настоящее время установлено в состоянии. Ваш пример ничего не обновляет. Я бы использовал Action Creator только для передачи состояния из его параметров.

В редукторе сделайте что-то вроде:

const state = this.state;
const apple = this.state.apples;

Если вам нужно выполнить действие с состоянием для ТИПА, на который вы ссылаетесь, сделайте это в редукторе.

Пожалуйста, поправьте меня, если я ошибаюсь!!!

Другие вопросы по тегам