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

296

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

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));
  }
}
ffxsam
источник

Ответы:

522

Существуют различные мнения о том, является ли доступ к создателям состояний в действии хорошей идеей:

  • Создатель Redux Дэн Абрамов считает, что это должно быть ограничено: «Несколько случаев, когда я считаю приемлемым, проверять кэшированные данные перед тем, как сделать запрос, или проверить, аутентифицированы ли вы (другими словами, выполнить условную диспетчеризацию). Я думаю, что передача данных, таких как state.something.itemsв создателе действий, определенно является анти-паттерном и не рекомендуется, потому что затеняет историю изменений: если есть ошибка и она itemsневерна, трудно отследить, откуда взялись эти неверные значения, потому что они уже часть действия, а не непосредственно вычисляется редуктором в ответ на действие. Так что делайте это с осторожностью ".
  • Текущий сопровождающий Redux Марк Эриксон говорит, что это нормально и даже рекомендуется использовать getStateв thunks - вот почему он существует . Он обсуждает плюсы и минусы доступа к состоянию в действии создателей в своем блоге « Идиоматическое 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 содержат информацию о логике разделения между создателями действий и редукторами, а также о случаях, когда это может быть полезно для использования getStateв создателе действий .

Дан Абрамов
источник
5
У меня есть одна ситуация, когда выбор чего-либо в компоненте может вызвать PUT или POST, в зависимости от того, содержит ли хранилище данные, относящиеся к компоненту. Лучше ли поместить бизнес-логику для выбора PUT / POST в компонент вместо создателя действий на основе thunk?
vicusbass
3
Какова лучшая практика? Сейчас я сталкиваюсь с аналогичной проблемой, когда я использую getState в своем создателе действий. В моем случае я использую его, чтобы определить, есть ли значения, если форма имеет ожидающие изменения (и если так, я отправлю действие, которое показывает диалог).
Арне Х. Битубекк
42
В магазине действий можно читать из магазина. Я бы посоветовал вам использовать селектор, чтобы вы не зависели от точной формы состояния.
Дан Абрамов
2
Я использую промежуточное программное обеспечение для отправки данных на mixpanel. Так что у меня есть мета-ключ внутри действия. Мне нужно передать различные переменные из состояния в mixpanel. Установка их на действие создателей, кажется, анти-паттерн. Каков наилучший подход для обработки таких случаев использования?
Аакаш Сигдел
2
О чувак! Я не завязываю, что получаю getState
избыточный
33

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

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_LOAD REQUEST_LOAD_SUCCESS REQUEST_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, чтобы вернуть функцию в действие создателя

Нур Саммур
источник
3
Могу ли я просто спросить, нужно ли проверять состояние состояния «загрузка», чтобы другой запрос ajax не выполнялся, пока первый завершается?
JoeTidee
@JoeTidee В примере отправка состояния загрузки выполнена. Если вы сделаете это действие, например, с помощью кнопки, вы бы проверили loading === trueналичие кнопки и отключили ее.
croff
4

Я согласен с @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);
Джейсон Оллсхорн
источник
Этот метод вполне логичен.
Ахмет Шимшек
Это правильный способ сделать это и как я это делаю. Создателю действия не нужно знать все состояние, только ту часть, которая имеет к нему отношение.
Пепел
3

Я хотел бы отметить, что читать из магазина не так уж и плохо - может быть, гораздо удобнее решить, что делать на основе хранилища, чем передать все компоненту, а затем в качестве параметра функция. Я полностью согласен с Дэном в том, что гораздо лучше не использовать хранилище в качестве синглтона, если только вы не уверены на 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())
    };
  },
});

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

Bloomca
источник
1

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

Вы можете получить состояние от редукторов к действиям, разделив действие на две отдельные функции: сначала запросите данные, затем выполните действие с данными. Вы можете сделать это с помощью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,
    }
}

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

Андрей Чоара
источник
0

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

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

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

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

SqueeDee
источник
0

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

Sam96
источник