Как показать индикатор загрузки в приложении React Redux при получении данных? [закрыто]

107

Я новичок в React / Redux. Я использую промежуточное ПО fetch api в приложении Redux для обработки API. Это ( redux-api-middleware ). Я думаю, что это хороший способ обрабатывать действия async api. Но я нахожу такие случаи, которые не могу разрешить самостоятельно.

Как говорится на домашней странице ( жизненный цикл ), жизненный цикл API выборки начинается с отправки действия CALL_API и заканчивается отправкой действия FSA.

Итак, мой первый случай показывает / скрывает предварительный загрузчик при получении API. Промежуточное ПО отправит действие FSA в начале и отправит действие FSA в конце. Оба действия принимаются редукторами, которые должны выполнять только некоторую нормальную обработку данных. Никаких операций пользовательского интерфейса, никаких операций. Возможно, мне следует сохранить статус обработки в состоянии, а затем отобразить их при обновлении магазина.

Но как это сделать? Поток компонентов реагирования на всю страницу? что случилось с обновлением магазина от других действий? Я имею в виду, что они больше похожи на события, чем на состояние!

Даже в худшем случае, что мне делать, если мне нужно использовать собственный диалог подтверждения или диалоговое окно с предупреждением в приложениях redux / response? Куда их ставить, действия или редукторы?

С наилучшими пожеланиями! Желаю ответить.

企业 应用 架构 模式 大师
источник
1
Откатил последнее изменение этого вопроса, поскольку оно изменило всю суть вопроса и ответы ниже.
Gregg B
Событие - это смена состояния!
企业 应用 架构 模式 大师
Взгляните на квестора. github.com/orar/questrar
Орар

Ответы:

152

Я имею в виду, что они больше похожи на события, чем на состояние!

Я бы так не сказал. Я думаю, что индикаторы загрузки - отличный пример пользовательского интерфейса, который легко описать как функцию состояния: в данном случае логической переменной. Хотя это правильный ответ , я хотел бы предоставить ему код.

В asyncпримере в Redux репо , редуктор обновляет поле под названиемisFetching :

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

Компонент использует connect()React Redux для подписки на состояние хранилища и возвращается isFetchingкак часть mapStateToProps()возвращаемого значения, поэтому оно доступно в свойствах подключенного компонента:

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

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

Наконец, компонент использует isFetchingопору в render()функции для отображения метки «Загрузка ...» (которая, вероятно, могла бы быть счетчиком):

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

Даже в худшем случае, что мне делать, если мне нужно использовать собственный диалог подтверждения или диалоговое окно с предупреждением в приложениях redux / response? Куда их ставить, действия или редукторы?

Любые побочные эффекты (и отображение диалогового окна, безусловно, является побочным эффектом) не относятся к редукторам. Думайте о редукторах как о пассивных «строителях государства». На самом деле они ничего не «делают».

Если вы хотите показать предупреждение, либо сделайте это из компонента перед отправкой действия, либо сделайте это из создателя действия. К моменту отправки действия уже слишком поздно выполнять побочные эффекты в ответ на него.

Для каждого правила есть исключение. Иногда логика побочных эффектов настолько сложна, что вы действительно хотите связать их либо с определенными типами действий, либо с конкретными редукторами. В этом случае обратите внимание на продвинутые проекты, такие как Redux Saga и Redux Loop . Делайте это только тогда, когда вам комфортно с ванильным Redux и у вас есть реальная проблема разрозненных побочных эффектов, которую вы хотели бы сделать более управляемой.

Дан Абрамов
источник
16
Что делать, если у меня выполняется несколько загрузок? Но тогда одной переменной было бы недостаточно.
philk
1
@philk, если у вас есть несколько выборок, вы можете сгруппировать их Promise.allв одно обещание, а затем отправить одно действие для всех выборок. Или вам нужно поддерживать несколько isFetchingпеременных в вашем состоянии.
Себастьян Лорбер,
2
Пожалуйста, внимательно посмотрите на пример, на который я ссылаюсь. Флагов больше одного isFetching. Он устанавливается для каждого набора извлекаемых объектов. Вы можете использовать композицию редуктора для реализации этого.
Дэн Абрамов
3
Обратите внимание, что если запрос не выполняется и RECEIVE_POSTSникогда не запускается, знак загрузки останется на месте, если вы не создали какой-то тайм-аут для отображения error loadingсообщения.
James111
2
@TomiS - я явно занесу в черный список все мои свойства isFetching из любой сохраняемости редукции, которую я использую.
duhseekoh
22

Отличный ответ Дан Абрамов! Просто хочу добавить, что я делал более или менее точно то же самое в одном из моих приложений (сохраняя isFetching как логическое) и в итоге мне пришлось сделать его целым числом (которое в конечном итоге считывается как количество невыполненных запросов) для поддержки нескольких одновременных Запросы.

с логическим значением:

запрос 1 запускается -> счетчик включен -> запускается запрос 2 -> заканчивается запрос 1 -> счетчик выключен -> запрос 2 завершается

с целым числом:

запрос 1 запускается -> счетчик включен -> запускается запрос 2 -> заканчивается запрос 1 -> заканчивается запрос 2 -> счетчик выключен

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
Нуно Кампос
источник
2
Это разумно. Однако чаще всего вы также хотите сохранить некоторые данные, которые вы извлекаете, в дополнение к флагу. На этом этапе вам понадобится более одного объекта с isFetchingфлагом. Если вы внимательно посмотрите на пример, который я связал, вы увидите, что существует не один объект, isFetchedа много: по одному на каждый субреддит (что и происходит в этом примере).
Дэн Абрамов
2
ой. да, я этого не заметил. однако в моем случае у меня есть одна глобальная запись isFetching в состоянии и запись в кеше, где хранятся полученные данные, и для моих целей меня действительно волнует только то, что происходит некоторая сетевая активность, на самом деле не имеет значения, для чего
Нуно Кампос
4
Ага! Это зависит от того, хотите ли вы отображать индикатор загрузки в одном или нескольких местах пользовательского интерфейса. Фактически, вы можете комбинировать два подхода и иметь как глобальный fetchCounterиндикатор выполнения в верхней части экрана, так и несколько специальных isFetchingфлагов для списков и страниц.
Дэн Абрамов
Если у меня есть запросы POST в более чем одном файле, как мне установить состояние isFetching, чтобы отслеживать его текущее состояние?
user989988
13

Хочу кое-что добавить. В реальном примере используется поле isFetchingв магазине, чтобы представить, когда происходит выборка коллекции элементов. Любая коллекция обобщается до paginationредуктора, который можно подключить к вашим компонентам, чтобы отслеживать состояние и показывать, загружается ли коллекция.

Со мной случилось так, что я хотел получить детали для конкретной сущности, которая не вписывается в шаблон нумерации страниц. Я хотел иметь состояние, показывающее, извлекаются ли данные с сервера, но также я не хотел иметь редуктор только для этого.

Чтобы решить эту проблему, я добавил еще один универсальный редуктор под названием fetching. Он работает аналогично редюсеру нумерации страниц, и его задача - просто наблюдать за набором действий и генерировать новое состояние с парами [entity, isFetching]. Это позволяет connectредуктору к любому компоненту и знать, получает ли приложение в настоящее время данные не только для коллекции, но и для определенной сущности.

Javivelasco
источник
2
Спасибо за ответ! Обработка загрузки отдельных предметов и их статус редко обсуждается!
Гилад Пелег
Когда у меня есть один компонент, который зависит от действий другого, быстрый и грязный выход - в вашем mapStateToProps, объедините их следующим образом: isFetching: posts.isFetching || comments.isFetching - теперь вы можете заблокировать взаимодействие с пользователем для обоих компонентов при обновлении любого из них.
Филип Мерфи
5

Я не сталкивался с этим вопросом до сих пор, но, поскольку ответ не принят, я брошу шляпу. Я написал инструмент для этой работы: react-loader-factory . Это немного больше, чем решение Абрамова, но оно более модульное и удобное, так как мне не хотелось думать после того, как я его написал.

Есть четыре больших части:

  • Заводской шаблон: это позволяет вам быстро вызвать одну и ту же функцию, чтобы установить, какие состояния означают «Загрузка» для вашего компонента и какие действия нужно выполнить. (Это предполагает, что компонент отвечает за запуск ожидаемых действий.)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • Wrapper: компонент, который производит Factory, является «компонентом более высокого порядка» (например, то, что connect()возвращается в Redux), так что вы можете просто прикрепить его к существующему материалу.const LoadingChild = loaderWrapper(ChildComponent);
  • Взаимодействие «действие / редуктор»: оболочка проверяет, содержит ли редуктор, к которому она подключена, ключевые слова, указывающие на то, что она не передает информацию компоненту, которому нужны данные. Ожидается, что действия, отправляемые оболочкой, будут создавать связанные ключевые слова ( например, как отправляет redux-api-middleware ACTION_SUCCESSи ACTION_REQUEST). (Вы можете отправлять действия в другое место и, конечно, просто отслеживать их из оболочки.)
  • Throbber: компонент, который вы хотите отобразить, пока данные, от которых зависит ваш компонент, не готовы. Я добавил туда небольшой div, чтобы вы могли протестировать его, не настраивая его.

Сам модуль не зависит от redux-api-middleware, но я использую его именно с этим, поэтому вот пример кода из README:

Компонент с оберткой Loader:

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

Редуктор для мониторинга загрузчика (хотя вы можете подключить его по-другому, если хотите):

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

Я ожидаю, что в ближайшем будущем я добавлю в модуль такие вещи, как тайм-аут и ошибка, но шаблон не будет сильно отличаться.


Краткий ответ на ваш вопрос:

  1. Свяжите рендеринг с кодом рендеринга - используйте обертку вокруг компонента, который нужно рендерить, с данными, подобными показанным выше.
  2. Добавьте редуктор, который делает статус запросов в приложении, о котором вы, возможно, заботитесь, легко усваиваемым, чтобы вам не приходилось слишком много думать о том, что происходит.
  3. События и состояние на самом деле не отличаются.
  4. Остальная часть вашей интуиции мне кажется правильной.
яркая звезда
источник
4

Неужели я единственный, кто думает, что индикаторы загрузки не подходят Redux? Я имею в виду, я не думаю, что это часть состояния приложения как такового ...

Теперь я работаю с Angular2, и что я делаю, так это то, что у меня есть служба «Загрузка», которая предоставляет различные индикаторы загрузки через RxJS BehaviourSubjects .. Я предполагаю, что механизм такой же, я просто не храню информацию в Redux.

Пользователи LoadingService просто подписываются на те события, которые они хотят слушать.

Мои создатели действий Redux вызывают LoadingService всякий раз, когда что-то нужно изменить. Компоненты UX подписываются на открытые наблюдаемые ...

Спок
источник
Вот почему мне нравится идея магазина, где можно опросить все действия (ngrx и redux-logic), сервис не работает, redux-logic - функциональный. Хорошее чтение
srghma
20
Привет, проверяю более чем через год, просто чтобы сказать, что я был очень неправ. Конечно, состояние UX принадлежит состоянию приложения. Насколько я мог быть глупым?
Spock
3

Вы можете добавить слушателей изменений в свои магазины, используя либо connect()React Redux, либо низкоуровневый store.subscribe()метод. У вас должен быть индикатор загрузки в вашем магазине, который обработчик изменений магазина может затем проверить и обновить состояние компонента. Затем компонент при необходимости отображает предварительный загрузчик в зависимости от состояния.

alertи confirmне должно быть проблемой. Они блокируют, и предупреждение даже не принимает никаких данных от пользователя. С помощью confirmвы можете установить состояние на основе того, что пользователь щелкнул, если выбор пользователя должен повлиять на рендеринг компонента. Если нет, вы можете сохранить выбор как переменную-член компонента для дальнейшего использования.

Милош Рашич
источник
о коде оповещения / подтверждения, куда они должны быть помещены, действиях или редукторах?
企业 应用 架构 模式 大师
Зависит от того, что вы хотите с ними делать, но, честно говоря, я бы поместил их в код компонента в большинстве случаев, поскольку они являются частью пользовательского интерфейса, а не уровня данных.
Милош Рашич
некоторые компоненты пользовательского интерфейса действуют, инициируя событие (событие изменения статуса), а не сам статус. Например, анимация, показывающая / скрывающая прелоадер. Как вы их обрабатываете?
企业 应用 架构 模式 大师
Если вы хотите использовать нереагирующий компонент в своем реагирующем приложении, обычно используемым решением является создание реагирующего компонента оболочки, а затем использование его методов жизненного цикла для инициализации, обновления и уничтожения экземпляра нереагирующего компонента. Большинство таких компонентов используют элементы-заполнители в DOM для инициализации, и вы должны визуализировать их в методе рендеринга компонента реакции. Вы можете узнать больше о методах жизненного цикла здесь: facebook.github.io/react/docs/component-specs.html
Милош Рашич
У меня есть случай: область уведомлений в правом верхнем углу, которая содержит одно сообщение уведомления, каждое сообщение появляется, а затем исчезает через 5 секунд. Этот компонент находится вне веб-просмотра, предоставляемого собственным приложением хоста. Он предоставляет некоторый интерфейс js, например addNofication(message). Другой случай - это предварительные загрузчики, которые также предоставляются собственным приложением хоста и запускаются его API-интерфейсом javascript. Я добавляю оболочку для этих api в componentDidUpdateкомпонент React. Как мне спроектировать свойства или состояние этого компонента?
企业 应用 架构 模式 大师
3

В нашем приложении есть три типа уведомлений, все из которых разработаны как аспекты:

  1. Индикатор загрузки (модальный или немодальный в зависимости от опоры)
  2. Всплывающее окно с ошибкой (модальное)
  3. Панель уведомлений (немодальная, самозакрывающаяся)

Все три из них находятся на верхнем уровне нашего приложения (Main) и подключены через Redux, как показано в приведенном ниже фрагменте кода. Эти свойства управляют отображением соответствующих аспектов.

Я разработал прокси, который обрабатывает все наши вызовы API, поэтому все ошибки isFetching и (api) опосредуются actionCreators, которые я импортирую в прокси. (Кроме того, я также использую webpack, чтобы создать имитацию службы поддержки для разработчиков, чтобы мы могли работать без зависимостей от сервера.)

Любое другое место в приложении, которое должно предоставить уведомление любого типа, просто импортирует соответствующее действие. Snackbar & Error имеют параметры для отображения сообщений.

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

) экспорт класса по умолчанию Main extends React.Component {

Dreculah
источник
Я работаю над аналогичной настройкой с отображением загрузчика / уведомлений. У меня проблемы; у вас есть суть или пример того, как вы выполняете эти задачи?
Aymen
2

Я сохраняю URL-адреса, такие как ::

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

И затем у меня есть запомненный селектор (через повторный выбор).

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

Чтобы сделать URL-адрес уникальным в случае POST, я передаю некоторую переменную в качестве запроса.

А там, где я хочу показать индикатор, я просто использую переменную getFetchCount

Серджиу
источник
1
Вы можете заменить Object.keys(items).filter(item => items[item] === true).length > 0 ? true : falseна Object.keys(items).every(item => items[item]), кстати.
Александр Анник
1
Я думаю, вы имели в виду someвместо every, но да, слишком много ненужных сравнений в первом предложенном решении. Object.entries(items).some(([url, fetching]) => fetching);
Рафаэль Поррас Лусена