在获取数据时如何在React Redux应用程序中显示加载指示器?[关闭]


107

我是React / Redux的新手。我在Redux应用程序中使用fetch api中间件来处理API。它是(redux-api-middleware)。我认为这是处理异步api动作的好方法。但是我发现有些情况我自己无法解决。

如首页(Lifecycle)所述,获取API的生命周期始于调度CALL_API操作,然后终止于调度FSA操作。

所以我的第一种情况是在获取API时显示/隐藏预加载器。中间件将在开始时调度FSA动作,在结束时调度FSA动作。这两个动作均由减速器接收,该减速器仅应执行一些正常的数据处理。没有UI操作,没有更多操作。也许我应该将处理状态保存为状态,然后在商店更新时呈现它们。

但是该怎么做呢?一个React组件流遍及整个页面吗?通过其他操作更新商店会发生什么?我的意思是,他们更像是事件而不是状态!

更糟糕的是,当我必须在Redux / React应用程序中使用本机确认对话框或警报对话框时,该怎么办?应该将它们放在哪里,采取什么行动或减少压力?

最好的祝愿!希望得到答复。


1
回滚对该问题的最后编辑,因为它更改了下面问题和答案的全部内容。
Gregg B

事件就是状态的改变!
企业应用架构模式大师

看看questrar。 github.com/orar/questrar
Orar

Answers:


152

我的意思是,他们更像是事件而不是状态!

我不会这么说。我认为加载指示器是UI的一个很好的例子,可以很容易地将其描述为状态函数:在这种情况下,是布尔变量。虽然这个答案是正确的,但我想提供一些代码。

在里面 async Redux repo示例中,reducer 更新了一个名为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()从阵营终极版订阅商店的状态和收益isFetching作为的一部分mapStateToProps()返回值,以便它在连接组件的道具可供选择:

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

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

最后,组件使用isFetchingrender()函数中 prop来呈现“ Loading ...”标签(可以想象是微调器):

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

更糟糕的是,当我必须在Redux / React应用程序中使用本机确认对话框或警报对话框时,该怎么办?应该将它们放在哪里,采取什么行动或减少压力?

任何副作用(并且显示对话框肯定是副作用)都不属于reducers。认为减速器是被动的“国家建设者”。他们并没有真正“做”事情。

如果希望显示警报,请在分派动作之前从组件执行此操作,或者从动作创建者执行此操作。在分发动作时,为响应该动作而执行副作用已经为时已晚。

对于每个规则,都有一个例外。有时,您的副作用逻辑如此复杂,以至于您实际上希望将它们与特定的动作类型或特定的reduce耦合。在这种情况下,请检查Redux SagaRedux Loop等高级项目。仅当您对香草Redux感到满意并真正希望解决分散的副作用时才这样做。


16
如果我要进行多次提取怎么办?那么,一个变量就不够了。
菲利普

1
@philk如果有多个访存,则可以将它们分组Promise.all为一个promise,然后为所有访存分派单个操作。或者您必须isFetching在您的状态中维护多个变量。
Sebastien Lorber

2
请仔细查看我链接到的示例。有一个以上的isFetching标志。为正在获取的每组对象设置该属性。您可以使用reducer组合来实现。
Dan Abramov

3
请注意,如果请求失败并且RECEIVE_POSTS从未触发,则除非您创建了某种超时来显示error loading消息,否则加载符号将保留在原位。
James111 '16

2
@TomiS-我将我正在使用的所有redux持久性显式地将所有isFetching属性列入黑名单。
duhseekoh

22

很好的答案Dan Abramov!只是想补充一点,就是我在一个应用程序中所做的工作或多或少完全相同(将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多个对象:每个subreddit 一个对象(在该示例中正在获取)。
Dan Abramov

2
哦。是的,我没有注意到。但是在我的情况下,我在状态中有一个全局isFetching条目,还有一个缓存条目,在该条目中存储了获取的数据,出于我的目的,我只真正在乎某些网络活动正在发生,这对什么都不重要
Nuno Campos

4
是的 这取决于您是否要在用户界面的一个或多个位置显示获取指示符。事实上,你可以结合这两种方法,并有两个全球性fetchCounter的屏幕和几个特定的顶部一些进度条isFetching标记列表和页面。
Dan Abramov

如果我在多个文件中有POST请求,如何设置isFetching的状态以跟踪其当前状态?
user989988 '18

13

我想加些东西。真实示例使用isFetching商店中的字段来表示何时获取项目集合。任何集合都可以概括为pagination简器,可将其连接到组件以跟踪状态并显示集合是否正在加载。

碰巧我想获取不适合分页模式的特定实体的详细信息。我想有一个状态来表示是否正在从服务器中获取详细信息,但是我也不想为此而使用化简器。

为了解决这个问题,我添加了另一个通用的reducer fetching。它的工作方式与分页减速器类似,它的职责只是观察一组动作并成对产生新状态[entity, isFetching]。这就使connectreducer可以使用任何组件,并知道该应用程序当前是否不仅在为集合而且为特定实体获取数据。


2
感谢你的回答!很少讨论处理单个项目的装载及其状态!
吉拉德·佩莱格

当我有一个依赖于另一个组件的操作的组件时,mapStateToProps中有一个快速而肮脏的出路,将它们像这样组合:isFetching:posts.isFetching || comments.isFetching-现在,您可以在两个组件中的任何一个被更新时阻止用户交互。
菲利普·墨菲

5

直到现在我都没有遇到这个问题,但是由于没有答案,我会戴上帽子。我为此工作编写了一个工具:react-loader-factory。它比Abramov的解决方案有更多的功能,但是更加模块化和方便,因为我不想在写完之后再思考。

有四大块:

  • 出厂模式:这使您可以快速调用相同的函数来设置组件的状态表示“正在加载”,以及调度哪些动作。(这假定组件负责启动其等待的操作。)const loaderWrapper = loaderFactory(actionsList, monitoredStates);
  • 包装器:工厂生产的组件是“高阶组件”(例如connect()Redux中返回的组件),因此您可以将其栓接到现有的东西上。const LoadingChild = loaderWrapper(ChildComponent);
  • Action / Reducer交互:包装器检查是否插入了它的reducer包含告诉其不要传递给需要数据的组件的关键字。包装程序分派的动作应产生关联的关键字(例如redux-api-middleware的分派ACTION_SUCCESS和方式ACTION_REQUEST)。(当然,您可以将操作分派到其他地方,如果需要的话,可以从包装器进行监视。)
  • Throbber:组件依赖的数据尚未准备好时要显示的组件。我在那里添加了一个小div,因此您可以测试它,而不必进行装配。

该模块本身独立于redux-api-middleware,但这就是我使用的模块,因此这是README的一些示例代码:

带有装载程序的组件将其包裹起来:

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公开了不同的加载指示器。

LoadingService的用户只需订阅他们想收听的事件。

每当需要更改时,我的Redux操作创建者就会调用LoadingService。UX组件订阅公开的可观察对象...


这就是为什么我喜欢存储的想法,其中可以轮询所有操作(ngrx和redux-logic),服务不起作用,redux-logic-起作用。朗读
srghma

20
嗨,一年多后回来检查,只是说我错了。当然,UX状态属于应用程序状态。我有多蠢?
Spock

3

您可以使用connect()React Redux或低级store.subscribe()方法将更改侦听器添加到您的商店。您应该在商店中有加载指示器,商店更改处理程序可以使用该指示器检查并更新组件状态。然后,如果需要,组件将根据状态呈现预加载器。

alert并且confirm不应该是一个问题。它们正在阻止并且警报甚至没有从用户那里获取任何输入。使用confirm,如果用户的选择会影响组件的呈现,则可以根据用户单击的内容来设置状态。如果没有,您可以将选择存储为组件成员变量,以备后用。


关于警报/确认代码,应将其放在何处,应采取的措施或减少措施?
企业应用架构模式大师2016年

取决于您要对它们进行什么处理,但是说实话,在大多数情况下,我将它们放在组件代码中,因为它们是UI的一部分,而不是数据层。
米洛什RASIC

一些UI组件通过触发事件(状态更改事件)而不是状态本身来起作用。例如动画,显示/隐藏预加载器。您如何处理它们?
企业应用架构模式大师2016年

如果要在react应用程序中使用非反应组件,通常的解决方案是制作包装的react组件,然后使用其生命周期方法初始化,更新和销毁该非反应组件的实例。大多数此类组件使用DOM中的占位符元素进行初始化,您可以在react组件的render方法中进行渲染。:你可以阅读更多关于生命周期方法在这里facebook.github.io/react/docs/component-specs.html
米洛什RASIC

我有一个案例:位于右上角的通知区域,其中包含一条通知消息,每条消息显示出来,然后在5秒钟后消失。该组件不在由主机本机应用程序提供的Web视图中。它提供了一些js接口,例如addNofication(message)。另一种情况是预加载器,它也由主机本机应用提供,并由其javascript API触发。我在componentDidUpdateReact组件中为这些api添加了包装器。如何设计该组件的道具或状态?
企业应用架构模式大师2016年

3

我们的应用程序中有三种类型的通知,所有这些通知均被设计为方面:

  1. 加载指示器(基于道具的模态或非模态)
  2. 错误弹出窗口(模态)
  3. 通知小吃吧(非模式,自动关闭)

所有这三个都在我们的应用程序(主)的顶级,并通过Redux进行了连接,如下面的代码片段所示。这些道具控制其相应方面的显示。

我设计了一个代理来处理我们所有的API调用,因此所有的isFetching和(api)错误都由导入到代理中的actionCreator介导。(顺便说一句,我还使用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扩展了React.Component {


我正在通过显示加载程序/通知进行类似的设置。我遇到了问题;您是否有完成这些任务的要旨或示例?
艾门(Aymen)'16

2

我正在保存以下网址:

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

为了使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);
拉斐尔·波拉斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.