Como mostrar um indicador de carregamento no aplicativo React Redux enquanto busca os dados? [fechadas]

107

Sou novo no React / Redux. Eu uso um middleware fetch api no aplicativo Redux para processar as APIs. É ( redux-api-middleware ). Acho que é uma boa maneira de processar ações de API assíncronas. Mas encontro alguns casos que não podem ser resolvidos sozinho.

Como diz a página inicial ( Ciclo de Vida ), um ciclo de vida da API de busca começa com o despacho de uma ação CALL_API e termina com o despacho de uma ação FSA.

Portanto, meu primeiro caso é mostrar / ocultar um pré-carregador ao buscar APIs. O middleware despachará uma ação FSA no início e despachará uma ação FSA no final. Ambas as ações são recebidas por redutores que deveriam estar realizando apenas algum processamento normal de dados. Sem operações de interface do usuário, sem mais operações. Talvez eu deva salvar o status de processamento no estado e renderizá-los ao atualizar a loja.

Mas como fazer isso? Um fluxo de componente de reação em toda a página? o que acontece com a atualização da loja de outras ações? Quero dizer, eles são mais como eventos do que estado!

Pior ainda, o que devo fazer quando tiver que usar a caixa de diálogo de confirmação nativa ou a caixa de diálogo de alerta em aplicativos redux / react? Onde eles devem ser colocados, ações ou redutores?

Muitas felicidades! Desejo uma resposta.

企业 应用 架构 模式 大师
fonte
1
Revertida a última edição desta pergunta, pois mudou todo o ponto da pergunta e as respostas abaixo.
Gregg B
Um evento é a mudança de estado!
企业 应用 架构 模式 大师
Dê uma olhada em questrar. github.com/orar/questrar
Orar

Respostas:

152

Quero dizer, eles são mais como eventos do que estado!

Eu não diria isso. Eu acho que os indicadores de carregamento são um ótimo caso de IU que é facilmente descrito como uma função de estado: neste caso, de uma variável booleana. Embora essa resposta esteja correta, gostaria de fornecer um código para acompanhá-la.

No asyncexemplo do repo Redux , o redutor atualiza um campo chamadoisFetching :

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: true,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: false,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt

O componente usa connect()do React Redux para assinar o estado da loja e retorna isFetchingcomo parte do mapStateToProps()valor de retorno para que esteja disponível nos props do componente conectado:

function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Finalmente, o componente usa isFetchingprop na render()função para renderizar um rótulo “Carregando ...” (que poderia ser um spinner):

{isEmpty
  ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
  : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
      <Posts posts={posts} />
    </div>
}

Pior ainda, o que devo fazer quando tiver que usar a caixa de diálogo de confirmação nativa ou a caixa de diálogo de alerta em aplicativos redux / react? Onde eles devem ser colocados, ações ou redutores?

Quaisquer efeitos colaterais (e mostrar uma caixa de diálogo é certamente um efeito colateral) não pertencem aos redutores. Pense nos redutores como “construtores de estado” passivos. Eles realmente não “fazem” coisas.

Se você deseja mostrar um alerta, faça isso de um componente antes de despachar uma ação ou faça isso de um criador de ação. No momento em que uma ação é despachada, é tarde demais para realizar os efeitos colaterais em resposta a ela.

Para cada regra, existe uma exceção. Às vezes, a lógica de seus efeitos colaterais é tão complicada que você realmente deseja acoplá-los a tipos de ação específicos ou a redutores específicos. Nesse caso, confira projetos avançados como Redux Saga e Redux Loop . Só faça isso quando você se sentir confortável com o Redux vanilla e tiver um problema real de efeitos colaterais dispersos que gostaria de tornar mais administrável.

Dan Abramov
fonte
16
E se eu tiver várias buscas em andamento? Então, uma variável não seria suficiente.
philk
1
@philk se você tiver múltiplas buscas, você pode agrupá-las Promise.allem uma única promessa e então despachar uma única ação para todas as buscas. Ou você tem que manter várias isFetchingvariáveis ​​em seu estado.
Sebastien Lorber
2
Por favor, dê uma olhada no exemplo para o qual estou vinculado. Há mais de uma isFetchingbandeira. Ele é definido para cada conjunto de objetos que estão sendo buscados. Você pode usar a composição do redutor para implementar isso.
Dan Abramov
3
Observe que, se a solicitação falhar e RECEIVE_POSTSnunca for disparada, o sinal de carregamento permanecerá no local, a menos que você tenha criado algum tipo de tempo limite para mostrar uma error loadingmensagem.
James111
2
@TomiS - Eu coloco na lista negra explicitamente todas as minhas propriedades isFetching de qualquer persistência de redux que estou usando.
duhseekoh
22

Ótima resposta Dan Abramov! Só quero acrescentar que estava fazendo mais ou menos exatamente isso em um dos meus aplicativos (mantendo isFetching como um booleano) e acabei tendo que torná-lo um inteiro (que acaba sendo lido como o número de solicitações pendentes) para suportar vários pedidos simultâneos solicitações de.

com booleano:

solicitação 1 inicia -> girador ligado -> solicitação 2 inicia -> solicitação 1 termina -> spinner desligado -> solicitação 2 termina

com inteiro:

solicitação 1 inicia -> spinner ligado -> solicitação 2 inicia -> solicitação 1 termina -> solicitação 2 termina -> spinner desligado

case REQUEST_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching + 1,
    didInvalidate: false
  })
case RECEIVE_POSTS:
  return Object.assign({}, state, {
    isFetching: state.isFetching - 1,
    didInvalidate: false,
    items: action.posts,
    lastUpdated: action.receivedAt
Nuno Campos
fonte
2
Isso é razoável. No entanto, na maioria das vezes, você também deseja armazenar alguns dados que busca além do sinalizador. Neste ponto, você precisará ter mais de um objeto com isFetchingbandeira. Se você olhar atentamente para o exemplo que vinculei, verá que não há um objeto com, isFetchedmas muitos: um por subreddit (que é o que está sendo buscado naquele exemplo).
Dan Abramov
2
oh. sim, eu não percebi isso. no entanto, no meu caso, tenho uma entrada isFetching global no estado e uma entrada de cache onde os dados obtidos são armazenados e, para os meus propósitos, só me importo se alguma atividade de rede está acontecendo, realmente não importa para quê
Nuno Campos
4
Sim! Depende se você deseja mostrar o indicador de busca em um ou mais lugares na IU. Na verdade, você pode combinar as duas abordagens e têm ambos um mundial fetchCounterpor alguma barra de progresso no topo da tela e várias específicas isFetchingbandeiras para listas e páginas.
Dan Abramov
Se eu tiver solicitações POST em mais de um arquivo, como definiria o estado de isFetching para manter o controle de seu estado atual?
user989988
13

Eu gostaria de acrescentar algo. O exemplo do mundo real usa um campo isFetchingna loja para representar quando uma coleção de itens está sendo buscada. Qualquer coleção é generalizada para um paginationredutor que pode ser conectado aos seus componentes para rastrear o estado e mostrar se uma coleção está carregando.

Aconteceu comigo que eu queria buscar detalhes para uma entidade específica que não se encaixa no padrão de paginação. Eu queria ter um estado representando se os detalhes estão sendo buscados no servidor, mas também não queria ter um redutor apenas para isso.

Para resolver isso, adicionei outro redutor genérico chamado fetching. Funciona de forma semelhante ao redutor de paginação e sua responsabilidade é apenas observar um conjunto de ações e gerar um novo estado com pares [entity, isFetching]. Isso permite ao connectredutor de qualquer componente saber se o aplicativo está buscando dados não apenas para uma coleção, mas para uma entidade específica.

Javivelasco
fonte
2
Obrigado pela resposta! Lidar com o carregamento de itens individuais e seu status raramente é discutido!
Gilad Peleg
Quando eu tenho um componente que depende da ação de outro, uma saída rápida e suja é em seu mapStateToProps combiná-los assim: isFetching: posts.isFetching || comments.isFetching - agora você pode bloquear a interação do usuário para ambos os componentes quando um deles está sendo atualizado.
Philip Murphy
5

Não me deparei com essa pergunta até agora, mas como nenhuma resposta é aceita, vou jogar meu chapéu. Eu escrevi uma ferramenta exatamente para este trabalho: react-loader-factory . É um pouco mais ativo do que a solução de Abramov, mas é mais modular e conveniente, já que não queria ter que pensar depois de escrevê-lo.

Existem quatro grandes peças:

  • Padrão de fábrica: permite que você chame rapidamente a mesma função para configurar quais estados significam "Carregando" para seu componente e quais ações despachar. (Isso pressupõe que o componente é responsável por iniciar as ações que ele espera.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: O componente que o Factory produz é um "componente de ordem superior" (como o que connect()retorna no Redux), de modo que você pode apenas parafusá-lo em seu material existente.const LoadingChild = loaderWrapper(ChildComponent);
  • Interação de ação / redutor: o wrapper verifica se um redutor ao qual está conectado contém palavras-chave que o informam para não passar para o componente que precisa de dados. Espera-se que as ações despachadas pelo wrapper produzam as palavras-chave associadas (a maneira como redux-api-middleware despacha ACTION_SUCCESSe ACTION_REQUEST, por exemplo). (Você pode enviar ações para outro lugar e apenas monitorar a partir do invólucro se quiser, é claro).
  • Throbber: o componente que você deseja exibir enquanto os dados dos quais seu componente depende não estão prontos. Eu adicionei um pequeno div para que você possa testá-lo sem ter que manipulá-lo.

O módulo em si é independente do redux-api-middleware, mas é com isso que eu o utilizo, então aqui está um exemplo de código do README:

Um componente com um carregador envolvendo-o:

import React from 'react';
import { myAsyncAction } from '../actions';
import loaderFactory from 'react-loader-factory';
import ChildComponent from './ChildComponent';

const actionsList = [myAsyncAction()];
const monitoredStates = ['ASYNC_REQUEST'];
const loaderWrapper = loaderFactory(actionsList, monitoredStates);

const LoadingChild = loaderWrapper(ChildComponent);

const containingComponent = props => {
  // Do whatever you need to do with your usual containing component 

  const childProps = { someProps: 'props' };

  return <LoadingChild { ...childProps } />;
}

Um redutor para o carregador monitorar (embora você possa conectá-lo de forma diferente, se desejar):

export function activeRequests(state = [], action) {
  const newState = state.slice();

  // regex that tests for an API action string ending with _REQUEST 
  const reqReg = new RegExp(/^[A-Z]+\_REQUEST$/g);
  // regex that tests for a API action string ending with _SUCCESS 
  const sucReg = new RegExp(/^[A-Z]+\_SUCCESS$/g);

  // if a _REQUEST comes in, add it to the activeRequests list 
  if (reqReg.test(action.type)) {
    newState.push(action.type);
  }

  // if a _SUCCESS comes in, delete its corresponding _REQUEST 
  if (sucReg.test(action.type)) {
    const reqType = action.type.split('_')[0].concat('_REQUEST');
    const deleteInd = state.indexOf(reqType);

    if (deleteInd !== -1) {
      newState.splice(deleteInd, 1);
    }
  }

  return newState;
}

Espero que em um futuro próximo adicionarei coisas como tempo limite e erro ao módulo, mas o padrão não será muito diferente.


A resposta curta à sua pergunta é:

  1. Vincule a renderização ao código de renderização - use um wrapper ao redor do componente que você precisa renderizar com os dados, como o que mostrei acima.
  2. Adicione um redutor que torne o status das solicitações em torno do aplicativo com o qual você pode se preocupar facilmente digerível, para que você não precise pensar muito sobre o que está acontecendo.
  3. Os eventos e o estado não são realmente diferentes.
  4. O resto de suas intuições parecem corretas para mim.
estrela Brilhante
fonte
4

Sou o único a pensar que os indicadores de carregamento não pertencem a uma loja Redux? Quer dizer, não acho que seja parte do estado de um aplicativo em si.

Agora, eu trabalho com Angular2, e o que eu faço é que eu tenho um serviço de "carregamento" que expõe diferentes indicadores de carregamento via RxJS BehaviourSubjects .. Eu acho que o mecanismo é o mesmo, eu só não armazeno a informação no Redux.

Os usuários do LoadingService apenas se inscrevem nos eventos que desejam ouvir.

Meus criadores de ação Redux chamam o LoadingService sempre que algo precisa mudar. Os componentes UX assinam os observáveis ​​expostos ...

Spock
fonte
é por isso que gosto da ideia de store, onde todas as ações podem ser pesquisadas (ngrx e redux-lógico), serviço não é funcional, redux-lógico - funcional. Boa leitura
srghma
20
Olá, volto mais de um ano depois, só para dizer que estava muito errado. É claro que o estado UX pertence ao estado do aplicativo. Quão estúpido eu poderia ser?
Spock
3

Você pode adicionar ouvintes de alterações às suas lojas, usando o connect()React Redux ou o store.subscribe()método de baixo nível . Você deve ter o indicador de carregamento em sua loja, que o manipulador de alterações da loja pode verificar e atualizar o estado do componente. O componente então renderiza o pré-carregador, se necessário, com base no estado.

alerte confirmnão deve ser um problema. Eles estão bloqueando e o alerta nem mesmo aceita nenhuma entrada do usuário. Com confirm, você pode definir o estado com base no que o usuário clicou se a escolha do usuário afetar a renderização do componente. Caso contrário, você pode armazenar a escolha como variável de membro do componente para uso posterior.

Miloš Rašić
fonte
sobre o código de alerta / confirmação, onde devem ser colocados, ações ou redutores?
企业 应用 架构 模式 大师
Depende do que você deseja fazer com eles, mas honestamente, eu os colocaria no código do componente na maioria dos casos, já que fazem parte da IU, não da camada de dados.
Miloš Rašić
alguns componentes da IU atuam disparando um evento (evento de mudança de status) em vez do próprio status. Como uma animação, mostrando / ocultando o pré-carregador. Como você os processa?
企业 应用 架构 模式 大师
Se você quiser usar um componente sem reação em seu aplicativo de reação, a solução geralmente usada é fazer um componente de reação do invólucro e, em seguida, usar seus métodos de ciclo de vida para inicializar, atualizar e destruir uma instância do componente sem reação. A maioria desses componentes usa elementos de espaço reservado no DOM para inicializar e você os renderizaria no método de renderização do componente react. Você pode ler mais sobre os métodos de ciclo de vida aqui: facebook.github.io/react/docs/component-specs.html
Miloš Rašić
Eu tenho um caso: uma área de notificação no canto superior direito, que contém uma mensagem de notificação, cada mensagem aparece e desaparece após 5 segundos. Este componente está fora da visualização da web, fornecido pelo aplicativo nativo do host. Ele fornece algumas interfaces js, como addNofication(message). Outro caso são os pré-carregadores que também são fornecidos pelo aplicativo nativo do host e acionados por sua API javascript. Eu adiciono um invólucro para essas APIs em componentDidUpdateum componente React. Como faço para projetar os adereços ou o estado desse componente?
企业 应用 架构 模式 大师
3

Temos três tipos de notificações em nosso aplicativo, todos concebidos como aspectos:

  1. Indicador de carregamento (modal ou não modal com base na prop)
  2. Popup de erro (modal)
  3. Snackbar de notificação (não modal, fechamento automático)

Todos os três estão no nível superior de nosso aplicativo (Principal) e conectados por meio do Redux, conforme mostrado no trecho de código abaixo. Esses adereços controlam a exibição de seus aspectos correspondentes.

Projetei um proxy que lida com todas as nossas chamadas de API, portanto, todos os erros isFetching e (api) são mediados com actionCreators que eu importo no proxy. (Como um aparte, eu também uso o webpack para injetar uma simulação do serviço de apoio para dev para que possamos trabalhar sem dependências do servidor.)

Qualquer outro local no aplicativo que precise fornecer qualquer tipo de notificação simplesmente importa a ação apropriada. Snackbar & Error têm parâmetros para que as mensagens sejam exibidas.

@connect(
// map state to props
state => ({
    isFetching      :state.main.get('isFetching'),   // ProgressIndicator
    notification    :state.main.get('notification'), // Snackbar
    error           :state.main.get('error')         // ErrorPopup
}),
// mapDispatchToProps
(dispatch) => { return {
    actions: bindActionCreators(actionCreators, dispatch)
}}

) exportar a classe padrão Main extends React.Component {

Dreculah
fonte
Estou trabalhando em uma configuração semelhante para mostrar um carregador / notificações. Estou tendo problemas; você teria uma ideia ou exemplo de como realizar essas tarefas?
Aymen,
2

Estou salvando os urls como ::

isFetching: {
    /api/posts/1: true,
    api/posts/3: false,
    api/search?q=322: true,
}

E então eu tenho um seletor memorizado (via nova seleção).

const getIsFetching = createSelector(
    state => state.isFetching,
    items => items => Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false
);

Para tornar o url único no caso de POST, passo alguma variável como consulta.

E onde quero mostrar um indicador, simplesmente uso a variável getFetchCount

Sergiu
fonte
1
Você pode substituir Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsepor Object.keys(items).every(item => items[item])pelo caminho.
Alexandre Annic
1
Acho que você quis dizer em somevez de every, mas sim, muitas comparações desnecessárias na primeira solução proposta. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena