Accéder à l'état Redux dans un créateur d'action?

296

Dites que j'ai ce qui suit:

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

Et dans ce créateur d'action, je veux accéder à l'état du magasin global (tous les réducteurs). Est-il préférable de faire ceci:

import store from '../store';

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

ou ca:

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

    dispatch(anotherAction(items));
  }
}
ffxsam
la source

Réponses:

522

Les opinions divergent quant à savoir si l'accès aux créateurs de l'état en action est une bonne idée:

  • Le créateur de Redux, Dan Abramov, estime que cela devrait être limité: "Les quelques cas d'utilisation où je pense que cela est acceptable sont pour vérifier les données mises en cache avant de faire une demande, ou pour vérifier si vous êtes authentifié (en d'autres termes, faire une répartition conditionnelle). Je pense que transmettre des données comme state.something.itemsdans un créateur d'action est définitivement un anti-modèle et est découragé car il a obscurci l'historique des modifications: s'il y a un bug et itemsqu'elles sont incorrectes, il est difficile de retracer d' viennent ces valeurs incorrectes parce qu'elles sont fait déjà partie de l'action, plutôt que d'être directement calculé par un réducteur en réponse à une action. Faites-le donc avec soin. "
  • Le mainteneur actuel de Redux, Mark Erikson, dit qu'il est getStatecorrect et même encouragé à l'utiliser dans les thunks - c'est pourquoi il existe . Il discute des avantages et des inconvénients de l'accès aux créateurs de l'état en action dans son article de blog Idiomatic Redux: Réflexions sur les thunks, les sagas, l'abstraction et la réutilisabilité .

Si vous trouvez que vous en avez besoin, les deux approches que vous avez suggérées conviennent. La première approche ne nécessite aucun middleware:

import store from '../store';

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

Cependant, vous pouvez voir qu'il repose sur storeun singleton exporté à partir d'un module. Nous ne le recommandons pas, car il est beaucoup plus difficile d' ajouter le rendu du serveur à votre application, car dans la plupart des cas, sur le serveur, vous souhaiterez avoir un magasin distinct par demande . Donc, bien que techniquement cette approche fonctionne, nous ne recommandons pas d'exporter un magasin à partir d'un module.

C'est pourquoi nous recommandons la deuxième approche:

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

    dispatch(anotherAction(items));
  }
}

Cela nécessiterait que vous utilisiez le middleware Redux Thunk mais cela fonctionne bien à la fois sur le client et sur le serveur. Vous pouvez en savoir plus sur Redux Thunk et pourquoi c'est nécessaire dans ce cas ici .

Idéalement, vos actions ne devraient pas être «grosses» et devraient contenir le moins d'informations possible, mais vous devriez vous sentir libre de faire ce qui vous convient le mieux dans votre propre application. La FAQ Redux contient des informations sur la répartition de la logique entre les créateurs d'action et les réducteurs et les moments où il peut être utile de l'utiliser getStatedans un créateur d'action .

Dan Abramov
la source
5
J'ai une situation où la sélection de quelque chose dans un composant peut déclencher un PUT ou un POST, selon que le magasin contient ou non des données liées au composant. Est-il préférable de mettre la logique métier pour la sélection PUT / POST dans le composant au lieu du créateur d'action basé sur thunk?
vicusbass
3
Quelle est la meilleure pratique? Je suis maintenant confronté au problème similaire lorsque j'utilise getState dans mon créateur d'action. Dans mon cas, je l'utilise pour déterminer si des valeurs si un formulaire a des modifications en attente (et si c'est le cas, je vais envoyer une action qui affiche une boîte de dialogue).
Arne H. Bitubekk
42
C'est bien de lire dans le magasin dans le créateur d'action. Je vous encourage à utiliser un sélecteur afin de ne pas dépendre de la forme exacte de l'état.
Dan Abramov
2
J'utilise un middleware pour envoyer des données à mixpanel. J'ai donc une méta-clé à l'intérieur de l'action. Je dois passer différentes variables de l'état au mixpanel. Les placer sur les créateurs d'action semble être un anti-modèle. Quelle serait la meilleure approche pour gérer ce type de cas d'utilisation?
Aakash Sigdel
2
Oh mec! Je n'ai pas noué que le thux redux reçoive getStatecomme deuxième paramètre, je craquais la tête, merci beaucoup
Lai32290
33

Lorsque votre scénario est simple, vous pouvez utiliser

import store from '../store';

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

Mais parfois, votre action creatorbesoin de déclencher plusieurs actions

par exemple, demande asynchrone, vous avez donc besoin d' REQUEST_LOAD REQUEST_LOAD_SUCCESS REQUEST_LOAD_FAILactions

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
                });
            }
        })
    }
}

Remarque: vous avez besoin de redux-thunk pour retourner la fonction dans le créateur d'action

Nour Sammour
la source
3
Puis-je simplement demander s'il doit y avoir un contrôle sur l'état de l'état «chargement» afin qu'aucune autre demande ajax ne soit faite pendant que la première se termine?
JoeTidee
@JoeTidee Dans l'exemple, l'envoi de l'état de chargement est effectué. Si vous effectuez cette action avec le bouton par exemple, vous vérifierez s'il loading === truey en a et désactivez le bouton.
croraf
4

Je suis d'accord avec @Bloomca. Passer la valeur nécessaire du magasin à la fonction de répartition comme argument semble plus simple que d'exporter le magasin. J'ai fait un exemple ici:

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);
Jason Allshorn
la source
Cette méthode est assez logique.
Ahmet Şimşek
C'est LA façon correcte de le faire et comment je le fais. Votre créateur d'action n'a pas besoin de connaître la totalité de l'état, seulement la partie qui lui est pertinente.
Ash
3

Je voudrais souligner qu'il n'est pas si mal de lire à partir du magasin - il pourrait être beaucoup plus pratique de décider de ce qui devrait être fait en fonction du magasin, que de tout passer au composant, puis en tant que paramètre de une fonction. Je suis entièrement d'accord avec Dan, qu'il est préférable de ne pas utiliser Store comme un seul ton, à moins que vous ne soyez sûr à 100% que vous n'utiliserez que pour le rendu côté client (sinon des bogues difficiles à tracer pourraient apparaître).

J'ai récemment créé une bibliothèque pour gérer la verbosité de redux, et je pense que c'est une bonne idée de tout mettre dans le middleware, donc vous avez tout comme injection de dépendance.

Donc, votre exemple ressemblera à ça:

import { createSyncTile } from 'redux-tiles';

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

Cependant, comme vous pouvez le voir, nous ne modifions pas vraiment les données ici, il y a donc de fortes chances que nous puissions simplement utiliser ce sélecteur à un autre endroit pour les combiner ailleurs.

Bloomca
la source
1

Présenter une autre façon de résoudre ce problème. Cela peut être meilleur ou pire que la solution de Dan, selon votre application.

Vous pouvez obtenir l'état des réducteurs dans les actions en divisant l'action en 2 fonctions distinctes: demander d'abord les données, deuxième agir sur les données. Vous pouvez le faire en utilisant redux-loop.

Tout d'abord «veuillez demander les données»

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

Dans le réducteur, interceptez la demande et fournissez les données à la deuxième étape en utilisant 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))
        }
    }
}

Avec les données en main, faites ce que vous vouliez au départ

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

J'espère que cela aide quelqu'un.

Andrei Cioara
la source
0

Je sais que je suis en retard à la fête ici, mais je suis venu ici pour des opinions sur mon propre désir d'utiliser l'état dans les actions, puis j'ai formé le mien, quand j'ai réalisé ce que je pense être le bon comportement.

C'est là qu'un sélecteur a le plus de sens pour moi. Votre composant qui émet cette demande doit être informé s'il est temps de le faire via la sélection.

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

Cela peut ressembler à une fuite d'abstractions, mais votre composant doit clairement envoyer un message et la charge utile du message doit contenir un état pertinent. Malheureusement, votre question n'a pas d'exemple concret car nous pourrions ainsi travailler sur un «meilleur modèle» de sélecteurs et d'actions.

SqueeDee
la source
0

Je voudrais suggérer une autre alternative que je trouve la plus propre, mais elle nécessite react-reduxou quelque chose de simulaire - j'utilise également quelques autres fonctionnalités sophistiquées en cours de route:

// 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);

Si vous souhaitez réutiliser ce que vous devrez extraire le spécifique mapState, mapDispatchet mergePropsen fonctions de réutiliser ailleurs, mais ce qui rend les dépendances parfaitement claires.

Sam96
la source