¿Cómo mostrar un indicador de carga en la aplicación React Redux mientras se obtienen los datos? [cerrado]

107

Soy nuevo en React / Redux. Utilizo un middleware fetch api en la aplicación Redux para procesar las API. Es ( redux-api-middleware ). Creo que es la mejor forma de procesar acciones de API asíncronas. Pero encuentro algunos casos que no puedo resolver por mí mismo.

Como dice la página de inicio ( ciclo de vida ), un ciclo de vida de la API de recuperación comienza con el envío de una acción CALL_API y termina con el envío de una acción FSA.

Entonces, mi primer caso es mostrar / ocultar un precargador al buscar API. El middleware enviará una acción FSA al principio y enviará una acción FSA al final. Ambas acciones son recibidas por reductores que solo deberían realizar un procesamiento normal de datos. Sin operaciones de IU, no más operaciones. Tal vez debería guardar el estado de procesamiento en el estado y luego representarlos cuando se actualice la tienda.

pero como hacer esto? ¿Un componente de reacción fluye por toda la página? ¿Qué sucede con la actualización de la tienda a partir de otras acciones? ¡Quiero decir que son más eventos que estados!

Incluso en el peor de los casos, ¿qué debo hacer cuando tengo que usar el diálogo de confirmación nativo o el diálogo de alerta en las aplicaciones redux / react? ¿Dónde deben colocarse, acciones o reductores?

¡Los mejores deseos! Deseo de responder.

企业 应用 架构 模式 大师
fuente
1
Se revirtió la última edición de esta pregunta, ya que cambió todo el punto de la pregunta y las respuestas a continuación.
Gregg B
¡Un evento es el cambio de estado!
企业 应用 架构 模式 大师
Eche un vistazo a questrar. github.com/orar/questrar
Orar

Respuestas:

152

¡Quiero decir que son más eventos que estados!

Yo no lo diría. Creo que los indicadores de carga son un gran caso de IU que se describe fácilmente como una función de estado: en este caso, de una variable booleana. Si bien esta respuesta es correcta, me gustaría proporcionar un código que la acompañe.

En el asyncejemplo en el repositorio de Redux , reducer actualiza un campo llamadoisFetching :

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

El componente usa connect()React Redux para suscribirse al estado de la tienda y regresa isFetchingcomo parte del mapStateToProps()valor de retorno para que esté disponible en los accesorios del 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, el componente usa isFetchingprop en la render()función para representar una etiqueta "Cargando ..." (que posiblemente podría ser una ruleta):

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

Incluso en el peor de los casos, ¿qué debo hacer cuando tengo que usar el diálogo de confirmación nativo o el diálogo de alerta en las aplicaciones redux / react? ¿Dónde deben colocarse, acciones o reductores?

Cualquier efecto secundario (y mostrar un diálogo es ciertamente un efecto secundario) no pertenece a los reductores. Piense en los reductores como "constructores de estado" pasivos. Realmente no "hacen" cosas.

Si desea mostrar una alerta, hágalo desde un componente antes de enviar una acción o desde un creador de acciones. Cuando se envía una acción, es demasiado tarde para realizar efectos secundarios en respuesta a ella.

Para cada regla, hay una excepción. A veces, la lógica de los efectos secundarios es tan complicada que en realidad desea combinarlos con tipos de acción específicos o con reductores específicos. En este caso, consulte proyectos avanzados como Redux Saga y Redux Loop . Solo haga esto cuando se sienta cómodo con Vanilla Redux y tenga un problema real de efectos secundarios dispersos que le gustaría hacer más manejable.

Dan Abramov
fuente
16
¿Qué pasa si tengo varias recuperaciones en curso? Entonces, una variable no sería suficiente.
philk
1
@philk, si tiene varias recuperaciones, puede agruparlas Promise.allen una única promesa y luego enviar una única acción para todas las recuperaciones. O tienes que mantener múltiples isFetchingvariables en tu estado.
Sebastien Lorber
2
Por favor, observe detenidamente el ejemplo al que tengo un vínculo. Hay más de una isFetchingbandera. Se establece para cada conjunto de objetos que se busca. Puede usar la composición reductora para implementar eso.
Dan Abramov
3
Tenga en cuenta que si la solicitud falla y RECEIVE_POSTSnunca se activa, la señal de carga permanecerá en su lugar a menos que haya creado algún tipo de tiempo de espera para mostrar un error loadingmensaje.
James111
2
@TomiS: incluyo explícitamente en la lista negra todas mis propiedades de isFetching de cualquier persistencia de redux que esté usando.
duhseekoh
22

¡Gran respuesta, Dan Abramov! Solo quiero agregar que estaba haciendo más o menos exactamente eso en una de mis aplicaciones (manteniendo isFetching como booleano) y terminé teniendo que convertirlo en un número entero (que termina leyendo como el número de solicitudes pendientes) para admitir múltiples solicitudes simultáneas peticiones.

con booleano:

solicitud 1 comienza -> ruleta encendida -> solicitud 2 comienza -> solicitud 1 termina -> ruleta apagada -> solicitud 2 termina

con entero:

solicitud 1 comienza -> ruleta activada -> solicitud 2 comienza -> solicitud 1 termina -> solicitud 2 termina -> ruleta desactivada

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
fuente
2
Esto es razonable. Sin embargo, la mayoría de las veces también desea almacenar algunos datos que obtenga además del indicador. En este punto, necesitará tener más de un objeto con isFetchingbandera. Si observa de cerca el ejemplo al que vinculé, verá que no hay un objeto con isFetchedsino muchos: uno por subreddit (que es lo que se está recuperando en ese ejemplo).
Dan Abramov
2
Oh. sí, no me di cuenta de eso. sin embargo, en mi caso, tengo una entrada isFetching global en el estado y una entrada de caché donde se almacenan los datos obtenidos, y para mis propósitos solo me importa que esté sucediendo alguna actividad de red, realmente no importa para qué
Nuno Campos
4
¡Sí! Depende de si desea mostrar el indicador de recuperación en uno o en varios lugares de la interfaz de usuario. De hecho, puede combinar los dos enfoques y tener tanto una fetchCounterbarra de progreso global para algunos en la parte superior de la pantalla como varios isFetchingindicadores específicos para listas y páginas.
Dan Abramov
Si tengo solicitudes POST en más de un archivo, ¿cómo puedo configurar el estado de isFetching para realizar un seguimiento de su estado actual?
user989988
13

Me gustaría agregar algo. El ejemplo del mundo real utiliza un campo isFetchingen la tienda para representar cuándo se está recuperando una colección de artículos. Cualquier colección se generaliza a un paginationreductor que se puede conectar a sus componentes para rastrear el estado y mostrar si se está cargando una colección.

Me sucedió que quería obtener detalles para una entidad específica que no encaja en el patrón de paginación. Quería tener un estado que representara si los detalles se están obteniendo del servidor, pero tampoco quería tener un reductor solo para eso.

Para solucionar esto agregué otro reductor genérico llamado fetching. Funciona de manera similar al reductor de paginación y su responsabilidad es simplemente observar un conjunto de acciones y generar un nuevo estado con pares [entity, isFetching]. Eso permite al connectreductor a cualquier componente y saber si la aplicación está obteniendo datos no solo para una colección sino para una entidad específica.

javivelasco
fuente
2
¡Gracias por la respuesta! ¡El manejo de la carga de artículos individuales y su estado rara vez se discute!
Gilad Peleg
Cuando tengo un componente que depende de la acción de otro, una salida rápida y sucia es en su mapStateToProps combínelos así: isFetching: posts.isFetching || comments.isFetching: ahora puede bloquear la interacción del usuario para ambos componentes cuando se actualiza cualquiera de ellos.
Philip Murphy
5

No me encontré con esta pregunta hasta ahora, pero como no se acepta ninguna respuesta, arrojaré mi sombrero. Escribí una herramienta para este mismo trabajo: react-loader-factory . Tiene un poco más de funcionamiento que la solución de Abramov, pero es más modular y conveniente, ya que no quería tener que pensar después de escribirlo.

Hay cuatro piezas grandes:

  • Patrón de fábrica: esto le permite llamar rápidamente a la misma función para configurar qué estados significan "Cargando" para su componente y qué acciones enviar. (Esto supone que el componente es responsable de iniciar las acciones que espera).const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: El componente que produce Factory es un "componente de orden superior" (como lo que connect()devuelve Redux), por lo que puede atornillarlo a sus cosas existentes.const LoadingChild = loaderWrapper(ChildComponent);
  • Interacción acción / reductor: el contenedor verifica si un reductor al que está conectado contiene palabras clave que le indiquen que no pase al componente que necesita datos. Se espera que las acciones enviadas por el contenedor produzcan las palabras clave asociadas (la forma en que redux-api-middleware distribuye ACTION_SUCCESSy ACTION_REQUEST, por ejemplo). (Por supuesto, podría enviar acciones a otro lugar y simplemente monitorear desde el contenedor si lo desea).
  • Throbber: el componente que desea que aparezca mientras los datos de los que depende su componente no están listos. Agregué un pequeño div allí para que puedas probarlo sin tener que armarlo.

El módulo en sí es independiente de redux-api-middleware, pero para eso lo uso, así que aquí hay un código de muestra del README:

Un componente con un cargador envolviéndolo:

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

Un reductor para que lo monitoree el cargador (aunque puede cablearlo de manera diferente si lo desea):

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 en un futuro cercano agregue cosas como tiempo de espera y error al módulo, pero el patrón no será muy diferente.


La respuesta corta a su pregunta es:

  1. Vincular el renderizado al código de renderizado: use un contenedor alrededor del componente que necesita renderizar con los datos como el que mostré arriba.
  2. Agregue un reductor que haga que el estado de las solicitudes alrededor de la aplicación que le interese sea fácil de digerir, para que no tenga que pensar demasiado en lo que está sucediendo.
  3. Los eventos y el estado no son realmente diferentes.
  4. El resto de tus intuiciones me parecen correctas.
Lucero
fuente
4

¿Soy el único que piensa que los indicadores de carga no pertenecen a una tienda Redux? Quiero decir, no creo que sea parte del estado de una aplicación per se ...

Ahora, trabajo con Angular2, y lo que hago es que tengo un servicio de "Cargando" que expone diferentes indicadores de carga a través de RxJS BehaviourSubjects .. Supongo que el mecanismo es el mismo, simplemente no almaceno la información en Redux.

Los usuarios de LoadingService simplemente se suscriben a los eventos que desean escuchar.

Mis creadores de acciones de Redux llaman a LoadingService cuando las cosas necesitan cambiar. Los componentes de UX se suscriben a los observables expuestos ...

Spock
fuente
es por eso que me gusta la idea de tienda, donde todas las acciones pueden ser consultadas (ngrx y redux-logic), el servicio no es funcional, redux-logic - funcional. Buena lectura
srghma
20
Hola, volviendo más de un año después, solo para decir que estaba muy equivocado. Por supuesto, el estado de UX pertenece al estado de la aplicación. ¿Qué tan estúpido pude ser?
Spock
3

Puede agregar oyentes de cambios a sus tiendas, usando connect()React Redux o el store.subscribe()método de bajo nivel . Debe tener el indicador de carga en su tienda, que el administrador de cambios de tienda puede luego verificar y actualizar el estado del componente. El componente luego procesa el precargador si es necesario, según el estado.

alerty confirmno debería ser un problema. Están bloqueando y la alerta ni siquiera toma ninguna entrada del usuario. Con confirm, puede establecer el estado en función de lo que el usuario haya hecho clic si la elección del usuario debe afectar la representación del componente. De lo contrario, puede almacenar la elección como variable miembro de componente para su uso posterior.

Miloš Rašić
fuente
sobre el código de alerta / confirmación, ¿dónde deben colocarse, acciones o reductores?
企业 应用 架构 模式 大师
Depende de lo que quieras hacer con ellos, pero honestamente los pondría en el código de componente en la mayoría de los casos, ya que son parte de la interfaz de usuario, no de la capa de datos.
Miloš Rašić
algunos componentes de la interfaz de usuario actúan activando un evento (evento de cambio de estado) en lugar del estado en sí. Como una animación, mostrando / ocultando el precargador. ¿Cómo los procesa?
企业 应用 架构 模式 大师
Si desea utilizar un componente que no reacciona en su aplicación de reacción, la solución que se utiliza generalmente es hacer que un componente de reacción sea un contenedor y luego use sus métodos de ciclo de vida para inicializar, actualizar y destruir una instancia del componente que no reacciona. La mayoría de estos componentes usan elementos de marcador de posición en el DOM para inicializar, y los representaría en el método de representación del componente de reacción. Puede leer más sobre los métodos del ciclo de vida aquí: facebook.github.io/react/docs/component-specs.html
Miloš Rašić
Tengo un caso: un área de notificación en la esquina superior derecha, que contiene un mensaje de notificación, cada mensaje aparece y luego desaparece después de 5 segundos. Este componente está fuera de la vista web, proporcionado por la aplicación nativa del host. Proporciona una interfaz js como addNofication(message). Otro caso son los precargadores que también son proporcionados por la aplicación nativa del host y activados por su API de JavaScript. Agrego un contenedor para esas API, en componentDidUpdateun componente React. ¿Cómo diseño los accesorios o el estado de este componente?
企业 应用 架构 模式 大师
3

Tenemos tres tipos de notificaciones en nuestra aplicación, todas ellas diseñadas como aspectos:

  1. Indicador de carga (modal o no modal basado en prop)
  2. Ventana emergente de error (modal)
  3. Snackbar de notificación (no modal, cierre automático)

Los tres están en el nivel superior de nuestra aplicación (principal) y están conectados a través de Redux como se muestra en el siguiente fragmento de código. Estos accesorios controlan la visualización de sus aspectos correspondientes.

Diseñé un proxy que maneja todas nuestras llamadas API, por lo tanto, todos los errores de isFetching y (api) están mediados por actionCreators que importo en el proxy. (Aparte, también uso webpack para inyectar una simulación del servicio de respaldo para el desarrollador para que podamos trabajar sin dependencias del servidor).

Cualquier otro lugar de la aplicación que necesite proporcionar algún tipo de notificación simplemente importa la acción correspondiente. Snackbar & Error tienen parámetros para que se muestren los mensajes.

@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 clase predeterminada Main extiende React.Component {

Dreculah
fuente
Estoy trabajando en una configuración similar con mostrar un cargador / notificaciones. Estoy teniendo problemas; ¿Tiene una idea o un ejemplo de cómo realiza estas tareas?
Aymen
2

Estoy guardando las URL como:

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

Y luego tengo un selector memorizado (a través de reselección).

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

Para que la URL sea única en caso de POST, paso alguna variable como consulta.

Y donde quiero mostrar un indicador, simplemente uso la variable getFetchCount

Sergiu
fuente
1
Puedes reemplazarlo Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falsepor Object.keys(items).every(item => items[item])cierto.
Alexandre Annic
1
Creo que quiso decir en somelugar de every, pero sí, demasiadas comparaciones no necesarias en la primera solución propuesta. Object.entries(items).some(([url, fetching]) => fetching);
Rafael Porras Lucena