Acessando o estado Redux em um criador de ações?

296

Digamos que tenho o seguinte:

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

E nesse criador de ações, desejo acessar o estado global da loja (todos os redutores). É melhor fazer isso:

import store from '../store';

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

ou isto:

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

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

Respostas:

522

Existem opiniões divergentes sobre se o acesso ao estado nos criadores de ação é uma boa ideia:

  • O criador do Redux, Dan Abramov, acha que deve ser limitado: "Os poucos casos de uso em que eu acho aceitável são verificar dados em cache antes de fazer uma solicitação ou verificar se você está autenticado (em outras palavras, fazendo um envio condicional). Eu acho que a transmissão de dados , como state.something.itemsem um criador de ação, é definitivamente um antipadrão e é desencorajada porque obscureceu o histórico de alterações: se há um erro e itemsestá incorreto, é difícil rastrear de onde vêm esses valores incorretos porque estão já faz parte da ação, em vez de ser diretamente calculado por um redutor em resposta a uma ação. Faça isso com cuidado ".
  • O atual mantenedor do Redux, Mark Erikson, diz que está bem e até incentivado a usar getStateem thunks - é por isso que existe . Ele discute os prós e os contras de acessar os criadores de ações em estado em seu blog Idiomatic Redux: Pensamentos sobre Thunks, Sagas, Abstração e Reutilização .

Se você achar que precisa disso, as duas abordagens sugeridas estão bem. A primeira abordagem não requer nenhum middleware:

import store from '../store';

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

No entanto, você pode ver que ele depende de storeser um singleton exportado de algum módulo. Não recomendamos isso porque torna muito mais difícil adicionar a renderização do servidor ao seu aplicativo, porque na maioria dos casos, no servidor, você deseja ter um armazenamento separado por solicitação . Portanto, embora tecnicamente essa abordagem funcione, não recomendamos exportar uma loja de um módulo.

É por isso que recomendamos a segunda abordagem:

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

    dispatch(anotherAction(items));
  }
}

Exigiria que você usasse o middleware Redux Thunk, mas funciona bem no cliente e no servidor. Você pode ler mais sobre o Redux Thunk e por que é necessário neste caso aqui .

Idealmente, suas ações não devem ser “gordas” e devem conter o mínimo de informações possível, mas você deve se sentir livre para fazer o que funcionar melhor para você em seu próprio aplicativo. As Perguntas frequentes do Redux contêm informações sobre a lógica de divisão entre criadores e redutores de ação e horários em que pode ser útil usá-lo getStateem um criador de ação .

Dan Abramov
fonte
5
Tenho uma situação ao selecionar algo em um componente que pode acionar um PUT ou um POST, dependendo se o armazenamento contém ou não dados relacionados ao componente. É melhor colocar a lógica de negócios para a seleção PUT / POST no componente em vez do criador de ação baseado em thunk?
vicusbass 3/16/16
3
Qual é a melhor prática? Agora estou enfrentando o problema semelhante ao usar o getState no meu criador de ações. No meu caso, eu o uso para determinar se os valores de um formulário estão com alterações pendentes (e, se houver, enviarei uma ação que mostra uma caixa de diálogo).
Arne H. Bitubekk,
42
É bom ler da loja no criador da ação. Recomendamos que você use um seletor para não depender da forma exata do estado.
Dan Abramov 06/06
2
Estou usando um middleware para enviar dados ao mixpanel. Então, eu tenho uma meta-chave dentro da ação. Eu preciso passar em diferentes variáveis ​​do estado para o mixpanel. Colocá-los nos criadores de ação parece ser um antipadrão. Qual seria a melhor abordagem para lidar com esse tipo de casos de uso?
Aakash Sigdel 21/09/16
2
Oh cara! Eu não fiz nó que redux conversão recebem getStatecomo o segundo parâmetro, eu estava rachando minha cabeça, obrigado assim tanto
Lai32290
33

Quando seu cenário é simples, você pode usar

import store from '../store';

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

Mas às vezes sua action creatornecessidade de acionar várias ações

por exemplo, solicitação assíncrona para que você precise de REQUEST_LOAD REQUEST_LOAD_SUCCESS REQUEST_LOAD_FAILações

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: você precisa de redux-thunk para retornar a função no criador de ações

Nour Sammour
fonte
3
Posso apenas perguntar se deve haver uma verificação no status do estado 'loading' para que outra solicitação de ajax não seja feita enquanto a primeira estiver terminando?
precisa saber é o seguinte
@JoeTidee No exemplo, o envio do estado de carregamento é feito. Se você executar esta ação com o botão, por exemplo, verificará se loading === trueexiste e desabilitará o botão.
Croraf 18/08/19
4

Eu concordo com @Bloomca. Passar o valor necessário da loja para a função de expedição como argumento parece mais simples do que exportar a loja. Eu fiz um exemplo aqui:

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
fonte
Este método é bastante lógico.
Ahmet Şimşek
Esta é a maneira correta de fazê-lo e como faço. Seu criador de ação não precisa conhecer todo o estado, apenas o que é relevante para ele.
Ash
3

Gostaria de salientar que não é tão ruim de ler na loja - pode ser muito mais conveniente decidir o que deve ser feito com base na loja, do que passar tudo para o componente e depois como um parâmetro de uma função. Eu concordo completamente com Dan, que é muito melhor não usar store como um singletone, a menos que você tenha 100% de certeza de que usará apenas para renderização do lado do cliente (caso contrário, poderá ser difícil rastrear erros).

Eu criei uma biblioteca recentemente para lidar com a verbosidade do redux, e acho que é uma boa ideia colocar tudo no middleware, para que você tenha tudo como uma injeção de dependência.

Portanto, seu exemplo será assim:

import { createSyncTile } from 'redux-tiles';

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

No entanto, como você pode ver, não modificamos realmente os dados aqui; portanto, há uma boa chance de podermos usar esse seletor em outro local para combiná-lo em outro lugar.

Bloomca
fonte
1

Apresentando uma maneira alternativa de resolver isso. Isso pode ser melhor ou pior que a solução de Dan, dependendo do seu aplicativo.

Você pode obter o estado dos redutores para as ações dividindo a ação em 2 funções separadas: primeiro solicite os dados, depois atue nos dados. Você pode fazer isso usando redux-loop.

Primeiro 'peça gentilmente os dados'

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

No redutor, intercepte a solicitação e forneça os dados para a ação do segundo estágio usando 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))
        }
    }
}

Com os dados em mãos, faça o que você queria inicialmente

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

Espero que isso ajude alguém.

Andrei Cioara
fonte
0

Sei que estou atrasado para a festa aqui, mas vim aqui para opinar sobre meu próprio desejo de usar o estado em ações e depois me formei quando percebi o que acho que é o comportamento correto.

É aqui que um seletor faz mais sentido para mim. Seu componente que emite essa solicitação deve ser informado se é hora de emiti-la através da seleção.

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

Pode parecer um vazamento de abstrações, mas seu componente claramente precisa enviar uma mensagem e a carga útil da mensagem deve conter um estado pertinente. Infelizmente, sua pergunta não tem um exemplo concreto, pois podemos trabalhar com um 'modelo melhor' de seletores e ações dessa maneira.

SqueeDee
fonte
0

Gostaria de sugerir mais uma alternativa que eu acho mais limpa, mas requer react-reduxou algo semelhante - também estou usando alguns outros recursos interessantes ao longo do caminho:

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

Se você quiser reutilizar isso, você terá que extrair o específico mapState, mapDispatche mergePropsem funções de reutilizar em outros lugares, mas isso torna dependências perfeitamente claro.

Sam96
fonte