Redux: disparando ações em resposta a transições de rota no roteador reac

Criado em 7 jul. 2015  ·  26Comentários  ·  Fonte: reduxjs/redux

Oi pessoal,

Estou usando o react-router e redux no meu aplicativo mais recente e estou enfrentando alguns problemas relacionados às mudanças de estado necessárias com base nos parâmetros e consultas de url atuais.

Basicamente, eu tenho um componente que precisa atualizar seu estado toda vez que a url muda. O estado está sendo passado através de adereços por redux com o decorador assim

 @connect(state => ({
   campaigngroups: state.jobresults.campaigngroups,
   error: state.jobresults.error,
   loading: state.jobresults.loading
 }))

No momento, estou usando o método de ciclo de vida componentWillReceiveProps para responder às alterações de url provenientes do react-router, uma vez que react-router vai passar novos props para o manipulador quando o url muda em this.props.params e this.props.query - o principal problema com esta abordagem é que estou disparando uma ação neste método para atualizar o estado - que então vai e passa novos adereços o componente que irá acionar o mesmo método de ciclo de vida novamente - basicamente criando um infinito loop, atualmente estou definindo uma variável de estado para impedir que isso aconteça.

  componentWillReceiveProps(nextProps) {
    if (this.state.shouldupdate) {
      let { slug } = nextProps.params;
      let { citizenships, discipline, workright, location } = nextProps.query;
      const params = { slug, discipline, workright, location };
      let filters = this._getFilters(params);
      // set the state accroding to the filters in the url
      this._setState(params);
      // trigger the action to refill the stores
      this.actions.loadCampaignGroups(filters);
    }
  }

Existe uma abordagem padrão para acionar ações com base nas transições de rota OU posso ter o estado da loja diretamente conectado ao estado do componente em vez de passá-lo por meio de adereços? Tentei usar o método estático willTransitionTo , mas não tenho acesso a this.props.dispatch lá.

docs ecosystem question

Comentários muito úteis

@deowk Esse problema tem duas partes, eu diria. A primeira é que componentWillReceiveProps() não é uma forma ideal de responder a mudanças de estado - principalmente porque força você a pensar de forma imperativa , em vez de reativamente, como fazemos com Redux. A solução é armazenar as informações atuais do roteador (localização, parâmetros, consulta) _dentro_ de sua loja. Então, todo o seu estado está no mesmo lugar, e você pode se inscrever nele usando a mesma API Redux do resto dos seus dados.

O truque é criar um tipo de ação que dispara sempre que a localização do roteador muda. Isso é fácil na próxima versão 1.0 do React Router:

// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));

Agora, o estado da sua loja estará sempre sincronizado com o estado do roteador. Isso corrige a necessidade de reagir manualmente às alterações de parâmetros de consulta e setState() em seu componente acima - basta usar o Conector do Redux.

<Connector select={state => ({ filter: getFilters(store.router.params) })} />

A segunda parte do problema é que você precisa de uma maneira de reagir às mudanças de estado Redux fora da camada de visualização, digamos, para disparar uma ação em resposta a uma mudança de rota. Você pode continuar a usar o componentWillReceiveProps para casos simples como o que você descreve, se desejar.

Para qualquer coisa mais complicada, porém, eu recomendo usar RxJS se você estiver aberto a isso. É exatamente para isso que os observáveis ​​são projetados - fluxo de dados reativo.

Para fazer isso no Redux, primeiro crie uma sequência observável de estados de armazenamento. Você pode fazer isso usando observableFromStore() redux-rx.

import { observableFromStore } from 'redux-rx';
const state$ = observableFromStore(store);

Então é apenas uma questão de usar operadores observáveis ​​para inscrever-se em mudanças de estado específicas. Aqui está um exemplo de redirecionamento de uma página de login após um login bem-sucedido:

const didLogin$ = state$
  .distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
  .filter(state => state.loggedIn && state.router.path === '/login');

didLogin$.subscribe({
   router.transitionTo('/success');
});

Esta implementação é muito mais simples que a mesma funcionalidade usando padrões imperativos como componentDidReceiveProps() .

Espero que ajude!

Todos 26 comentários

@deowk Você poderia comparar this.props com nextProps para ver se os dados relevantes mudaram. Se não mudou, você não precisa acionar a ação novamente e pode escapar do loop infinito.

@johanneslumpe : No meu caso, campaigngroups é uma coleção bastante grande de objetos, parece meio ineficiente usar uma comparação profunda de objetos como uma condição desta forma?

@deowk se você usar dados imutáveis, você pode apenas comparar referências

@deowk Esse problema tem duas partes, eu diria. A primeira é que componentWillReceiveProps() não é uma forma ideal de responder a mudanças de estado - principalmente porque força você a pensar de forma imperativa , em vez de reativamente, como fazemos com Redux. A solução é armazenar as informações atuais do roteador (localização, parâmetros, consulta) _dentro_ de sua loja. Então, todo o seu estado está no mesmo lugar, e você pode se inscrever nele usando a mesma API Redux do resto dos seus dados.

O truque é criar um tipo de ação que dispara sempre que a localização do roteador muda. Isso é fácil na próxima versão 1.0 do React Router:

// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));

Agora, o estado da sua loja estará sempre sincronizado com o estado do roteador. Isso corrige a necessidade de reagir manualmente às alterações de parâmetros de consulta e setState() em seu componente acima - basta usar o Conector do Redux.

<Connector select={state => ({ filter: getFilters(store.router.params) })} />

A segunda parte do problema é que você precisa de uma maneira de reagir às mudanças de estado Redux fora da camada de visualização, digamos, para disparar uma ação em resposta a uma mudança de rota. Você pode continuar a usar o componentWillReceiveProps para casos simples como o que você descreve, se desejar.

Para qualquer coisa mais complicada, porém, eu recomendo usar RxJS se você estiver aberto a isso. É exatamente para isso que os observáveis ​​são projetados - fluxo de dados reativo.

Para fazer isso no Redux, primeiro crie uma sequência observável de estados de armazenamento. Você pode fazer isso usando observableFromStore() redux-rx.

import { observableFromStore } from 'redux-rx';
const state$ = observableFromStore(store);

Então é apenas uma questão de usar operadores observáveis ​​para inscrever-se em mudanças de estado específicas. Aqui está um exemplo de redirecionamento de uma página de login após um login bem-sucedido:

const didLogin$ = state$
  .distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
  .filter(state => state.loggedIn && state.router.path === '/login');

didLogin$.subscribe({
   router.transitionTo('/success');
});

Esta implementação é muito mais simples que a mesma funcionalidade usando padrões imperativos como componentDidReceiveProps() .

Espero que ajude!

Precisamos disso em novos documentos. Muito bem escrito.

@acdlite : Muito obrigado, seu conselho realmente me ajudou muito. No entanto, estou lutando contra o seguinte -> alterar o estado na loja acionando um criador de ação em resposta a uma mudança de url.

Eu quero acionar o criador de ação no método estático willTransitionTo, pois esta parece ser a única opção no react-router v0.13.3

class Results extends Component  {
..........
}

Results.willTransitionTo = function (transition, params, query) {
 // how do I get a reference to dispatch here?
};

@deowk Meu palpite é que você chama dispatch diretamente na instância redux:

const redux = createRedux(stores);
BrowserHistory.listen(location => redux.dispatch(routeLocationDidUpdate(location)));

@deowk Atualmente despacho minhas alterações de url no roteador react 0.13.3 assim:

Router.run(routes, Router.HistoryLocation, function(Handler, locationState) {
   dispatch(routeLocationDidUpdate(locationState))
   React.render(<Handler/>, appEl);
});

@acdlite muito bem explicado! : +1:
Concordo que deve estar nos novos documentos também :)

Apenas como uma observação, BrowserHistory.history() mudou para BrowserHistory.addChangeListener() .

https://github.com/rackt/react-router/blob/master/modules/BrowserHistory.js#L57

EDITAR:

Que pode ser assim:

history.addChangeListener(() => store.dispatch(routeLocationDidUpdate(location)));

E quanto ao estado inicial desse redutor, @pburtchaell?

Eu tenho a seguinte configuração:

// client.js
class App extends Component {
  constructor(props) {
    super(props);
    this.history = new HashHistory();
  }

  render() {
    return (
      <Provider store={store}>
         {renderRoutes.bind(null, this.history)}
      </Provider>
    );
  }
}

// routes.js
export default function renderRoutes(history) {

  // When the route changes, dispatch that information to the store.
  history.addChangeListener(() => store.dispatch(routeLocationDidUpdate(location)));

  return (
    <Router history={history}>
      <Route component={myAppView}>
        <Route path="/" component={myHomeView}  />
      </Route>
    </Router>
  );
};

Isso dispara o criador da ação routeLocationDidUpdate() sempre que a rota muda e quando o aplicativo é carregado inicialmente.

@pburtchaell você pode compartilhar seus routeLocationDidUpdate ?

@gyzerok Certo. É um criador de ação.

// actions/location.js
export function routeLocationDidUpdate(location) {
  return {
    type: 'LOCATION_UPDATE',
    payload: {
      ...location,
    },
  };
}

@pburtchaell então em seu exemplo eu não entendo onde você busca os dados necessários para uma rota particular

Aqui está uma versão completa do meu exemplo acima:

https://github.com/acdlite/redux-react-router/blob/master/README.md#bonus -reacting-to-state-changes-with-redux-rx

Presumo que @pburtchaell usaria uma versão assíncrona desse código para obter dados (de uma chamada REST, etc.). O que eu não tenho certeza é o que então? Suponho que então vá para uma loja, mas essa loja seria uma loja que eu então

<strong i="7">@connect</strong>

para entrar, digamos em

<Comments />

que já estaria 'conectado' (inscrito) em um commentStore? Ou apenas adicionar o registro dessas ações de localização no commentsStore?

Um url como

/comments/123

sempre será 'acoplado' ao componente de comentários, então como faço para reutilizá-lo? Desculpe se isso é idiota, muito confuso aqui. Que havia um exemplo sólido que pude encontrar.

Também estou lutando com isso, descobrindo como chamar meu método estático fetchData(dispatch, params) (estático, porque o servidor o chama para despachar ações FETCH assíncronas antes da renderização inicial).

Já que uma referência a dispatch existe dentro do contexto, estou pensando que a maneira mais limpa de obter isso para chamadas do lado do cliente de fetchData é um componente semelhante a Connector que chama fetchData ou shouldFetchData , ou fazer com que o retorno de chamada select receba uma referência para dispatch junto com state , para que possamos inspecionar o atual estado de store e despacha FETCH ações conforme necessário.

O último é mais conciso, mas quebra o fato de que select deve permanecer uma função pura. Vou olhar para o primeiro.

Minha solução é esta, embora totalmente adaptada à minha configuração (método estático fetchData(dispatch, state) que despacha ações _FETCH assíncronas), ele chamará quaisquer retornos de chamada passados ​​a ele com as propriedades apropriadas : gist.github.com/grrowl/6cca2162e468891d8128 - não usa willTransitionTo, entretanto, você não poderá atrasar a transição das páginas.

Definitivamente precisamos adicionar os documentos! Possível na seção "Usando com react-router".

Teremos uma maneira oficial de fazer isso após o lançamento 1.0.

Ótimo ouvir @gaearon.

Então é apenas uma questão de usar operadores observáveis ​​para inscrever-se em mudanças de estado específicas.

Eu tenho um stream observável configurado para manter minha loja em sincronia com qualquer mudança de rota, e eu tenho um stream configurado para monitorar minha loja também. Estou interessado em fazer a transição para uma nova rota quando um valor muda em minha loja, especificamente quando muda de undefined para uma string válida como resultado do envio bem-sucedido de um formulário em outro lugar em meu aplicativo.

Os dois fluxos estão configurados e funcionando, e a loja está sendo atualizada, mas eu sou novo no Rx embora, e tendo problemas com a consulta observável ... algum indicador? Estou no caminho certo? Tenho certeza que tem algo a ver com distinctUntilChanged mas está assando meu macarrão no momento, qualquer ajuda será bem-vinda :)

EDIT: Ack! Desculpe, respondeu minha própria pergunta. Pseudo código abaixo. Deixe-me saber se parece horrível.

const didChangeProject$ = state$
  .distinctUntilChanged(state => state.project._id)
  .filter(state => typeof state.project._id !== 'undefined');

Fechando em favor de # 177. Quando começarmos a documentar o roteamento, precisaremos cobrir todos os cenários: redirecionar na mudança de estado, redirecionar na solicitação de retorno de chamada, etc.

Eu sei que isso está fechado .. mas com React 0.14 e as várias dependências 1.0 em beta agora, há uma atualização para isso e se não, um tutorial sobre as novas habilidades em React 0.14, react-roteador, redux, etc pode ser escrito? Parece que muitos de nós estão tentando nos manter atualizados com as mudanças que estão por vir, enquanto aprendemos / entendemos tudo isso ao mesmo tempo. Eu sei que as coisas estão em um estado de fluxo (trocadilho .. uh .. não pretendido), mas parece que estou vendo peças de diferentes exemplos que não combinam e depois de vários dias ainda não consigo fazer meu aplicativo atualizado funcionar com roteamento. Provavelmente minha própria culpa, pois ainda estou tendo dificuldades para entender como tudo isso funciona, apenas esperando que um tutorial atualizado com as coisas mais recentes seja lançado em breve.

Até que haja um tutorial, fique à vontade para olhar para examples/real-world que contém o roteamento. Não é "o melhor e mais recente" agora, mas funciona bem.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

CellOcean picture CellOcean  ·  3Comentários

parallelthought picture parallelthought  ·  3Comentários

ilearnio picture ilearnio  ·  3Comentários

mickeyreiss-visor picture mickeyreiss-visor  ·  3Comentários

jimbolla picture jimbolla  ·  3Comentários