¿Accediendo al estado de Redux en un creador de acción?

296

Digamos que tengo lo siguiente:

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

Y en ese creador de acciones, quiero acceder al estado de la tienda global (todos los reductores). ¿Es mejor hacer esto?

import store from '../store';

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

o esto:

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

    dispatch(anotherAction(items));
  }
}
ffxsam
fuente

Respuestas:

522

Hay diferentes opiniones sobre si acceder a los creadores de estado en acción es una buena idea:

  • El creador de Redux, Dan Abramov, considera que debería ser limitado: "Los pocos casos de uso en los que creo que es aceptable es para verificar los datos almacenados en caché antes de realizar una solicitud, o para verificar si está autenticado (en otras palabras, hacer un envío condicional). Creo que pasar datos como state.something.itemsen un creador de acción es definitivamente un antipatrón y se desaconseja porque oscureció el historial de cambios: si hay un error y itemsson incorrectos, es difícil rastrear de dónde provienen esos valores incorrectos porque son ya forma parte de la acción, en lugar de ser directamente calculada por un reductor en respuesta a una acción. Así que hazlo con cuidado ".
  • El actual mantenedor de Redux, Mark Erikson, dice que está bien e incluso se recomienda usarlo getStateen thunks, por eso existe . Habla sobre los pros y los contras de acceder a los creadores del estado en acción en su blog Blogom Redatic: Pensamientos sobre Thunks, Sagas, Abstracción y Reutilización .

Si encuentra que necesita esto, ambos enfoques que sugirió están bien. El primer enfoque no requiere ningún middleware:

import store from '../store';

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

Sin embargo, puede ver que depende de storeser un singleton exportado desde algún módulo. No lo recomendamos porque hace que sea mucho más difícil agregar la representación del servidor a su aplicación, porque en la mayoría de los casos en el servidor querrá tener una tienda separada por solicitud . Entonces, aunque técnicamente este enfoque funciona, no recomendamos exportar una tienda desde un módulo.

Por eso recomendamos el segundo enfoque:

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

    dispatch(anotherAction(items));
  }
}

Te requeriría usar el middleware Redux Thunk, pero funciona bien tanto en el cliente como en el servidor. Puede leer más sobre Redux Thunk y por qué es necesario en este caso aquí .

Idealmente, sus acciones no deben ser "gordas" y deben contener la menor información posible, pero debe sentirse libre de hacer lo que mejor le funcione en su propia aplicación. Las preguntas frecuentes de Redux tienen información sobre la división de la lógica entre los creadores y reductores de acciones y los momentos en que puede ser útil usarlos getStateen un creador de acciones .

Dan Abramov
fuente
55
Tengo una situación en la que seleccionar algo en un componente podría desencadenar un PUT o un POST, dependiendo de si la tienda contiene o no datos relacionados con el componente. ¿Es mejor poner la lógica de negocios para la selección PUT / POST en el componente en lugar del creador de acciones basado en thunk?
vicusbass
3
cual es la mejor practica? Ahora me enfrento al problema similar en el que estoy usando getState en mi creador de acciones. En mi caso, lo uso para determinar si los valores si un formulario tiene cambios pendientes (y si es así, enviaré una acción que muestre un cuadro de diálogo).
Arne H. Bitubekk
42
Está bien leer de la tienda en el creador de acción. Te animo a que uses un selector para que no dependas de la forma exacta del estado.
Dan Abramov
2
Estoy usando un middleware para enviar datos a mixpanel. Entonces tengo una meta clave dentro de la acción. Necesito pasar diferentes variables del estado al panel de mezclas. Establecerlos en los creadores de acción parece ser un antipatrón. ¿Cuál sería el mejor enfoque para manejar este tipo de casos de uso?
Aakash Sigdel
2
¡Oh hombre! No hice el nudo que recibí el redux thunk getStatecomo segundo parámetro, me estaba partiendo la cabeza, muchas gracias
Lai32290
33

Cuando su escenario es simple, puede usar

import store from '../store';

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

Pero a veces tu action creatornecesidad de activar acciones múltiples

por ejemplo, solicitud asíncrona por lo que necesita REQUEST_LOAD REQUEST_LOAD_SUCCESS REQUEST_LOAD_FAILacciones

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

Nota: necesita redux-thunk para devolver la función en el creador de la acción

Nour Sammour
fuente
3
¿Puedo preguntar si debería verificarse el estado del estado 'cargando' para que no se realice otra solicitud ajax mientras la primera está terminando?
JoeTidee
@JoeTidee En el ejemplo, se realiza el envío del estado de carga. Si realiza esta acción con el botón, por ejemplo, comprobará si está loading === trueallí y deshabilitará el botón.
croraf
4

Estoy de acuerdo con @Bloomca. Pasar el valor necesario de la tienda a la función de despacho como argumento parece más simple que exportar la tienda. Hice un ejemplo aquí:

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
fuente
Este método es bastante lógico.
Ahmet Şimşek
Esta es LA forma correcta de hacerlo y cómo lo hago. Su creador de acciones no necesita conocer todo el estado, solo el bit que le es relevante.
Ash
3

Me gustaría señalar que no es tan malo leer de la tienda: podría ser mucho más conveniente decidir qué hacer en función de la tienda, que pasar todo al componente y luego como un parámetro de Una función. Estoy completamente de acuerdo con Dan en que es mucho mejor no usar store como singleteone, a menos que esté 100% seguro de que solo lo usará para la representación del lado del cliente (de lo contrario, podrían aparecer errores difíciles de rastrear).

Creé una biblioteca recientemente para tratar la verbosidad de redux, y creo que es una buena idea poner todo en el middleware, para que tenga todo como inyección de dependencia.

Entonces, su ejemplo se verá así:

import { createSyncTile } from 'redux-tiles';

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

Sin embargo, como puede ver, en realidad no modificamos los datos aquí, por lo que existe una buena posibilidad de que podamos usar este selector en otro lugar para combinarlo en otro lugar.

Bloomca
fuente
1

Presentando una forma alternativa de resolver esto. Esto puede ser mejor o peor que la solución de Dan, dependiendo de su aplicación.

Puede obtener el estado de los reductores en las acciones dividiendo la acción en 2 funciones separadas: primero solicite los datos, segundo acto sobre los datos. Puedes hacerlo usando redux-loop.

Primero 'solicite amablemente los datos'

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

En el reductor, intercepte la solicitud y proporcione los datos a la acción de la segunda etapa mediante el uso 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))
        }
    }
}

Con los datos en la mano, haga lo que quiera inicialmente

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

Espero que esto ayude a alguien.

Andrei Cioara
fuente
0

Sé que llego tarde a la fiesta aquí, pero vine aquí para opinar sobre mi propio deseo de usar el estado en las acciones, y luego formé el mío, cuando me di cuenta de lo que creo que es el comportamiento correcto.

Aquí es donde un selector tiene más sentido para mí. Su componente que emite esta solicitud debe ser informado si es hora de hacerlo a través de la selección.

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

Puede parecer una fuga de abstracciones, pero su componente claramente necesita enviar un mensaje y la carga útil del mensaje debe contener el estado pertinente. Desafortunadamente, su pregunta no tiene un ejemplo concreto porque podríamos trabajar a través de un 'mejor modelo' de selectores y acciones de esa manera.

SqueeDee
fuente
0

Me gustaría sugerir otra alternativa que considero más limpia, pero que requiere react-reduxo algo similar, también estoy usando algunas otras características sofisticadas en el camino:

// 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 desea volver a utilizar esto usted tiene que extraer el específico mapState, mapDispatchy mergePropsen las funciones de volver a utilizar en otros lugares, pero esto hace que las dependencias perfectamente claro.

Sam96
fuente