如何在React Redux应用中显示加载指示器以在获取数据时使用?

111

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

正如主页(生命周期)所说,fetch API的生命周期始于调度CALL_API动作并以调度FSA动作结束。

所以我的第一个问题是,在获取API时显示/隐藏预加载程序。中间件会在开始时分派FSA操作,并在结束时分派FSA操作。这两个操作均由减速器接收,减速器只应进行一些正常的数据处理。没有UI操作,没有更多操作。也许我应该将处理状态保存在状态中,然后当存储更新时呈现它们。

但是如何做到这一点?React组件是否可以遍布整个页面?从其他操作更新存储会发生什么情况?我的意思是它们更像事件而不是状态!

甚至更糟糕的情况是,当我必须在redux / react应用程序中使用本机确认对话框或警报对话框时,我该怎么办?它们应该放在操作或减速器中?

最好的祝福!期待回复。


1
回滚了对该问题的最后一次编辑,因为它改变了问题和下面的答案的整个重点。 - Gregg B
事件是状态的改变! - 企业应用架构模式大师
看一下 questrar。https://github.com/orar/questrar - Orar
8个回答

157
我觉得它们更像是事件而不是状态! 我不这么认为。我认为加载指示器是一个很好的 UI 案例,可以轻松地描述为状态的函数:在这种情况下,是一个布尔变量的函数。虽然 this answer 是正确的,但我想提供一些代码来配合它。 在 Redux 存储库中的 async example 中,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

该组件使用 React Redux 中的 connect() 方法订阅 store 的状态,并在 mapStateToProps() 返回值中 返回 isFetching,以便它可以在连接的组件的 props 中使用:
function mapStateToProps(state) {
  const { selectedReddit, postsByReddit } = state
  const {
    isFetching,
    lastUpdated,
    items: posts
  } = postsByReddit[selectedReddit] || {
    isFetching: true,
    items: []
  }

  return {
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  }
}

最后,该组件render()函数中使用isFetching属性来呈现一个“正在加载…”标签(也可以是旋转图标):
{isEmpty
  ? (isFetching ? <h2>Loading...</h2> : <h2>Empty.</h2>)
  : <div style={{ opacity: isFetching ? 0.5 : 1 }}>
      <Posts posts={posts} />
    </div>
}

即使是更糟的情况,当我在redux/react应用程序中必须使用本地确认对话框或警报对话框时,我该怎么办?它们应该放在actions还是reducers中? 任何副作用(显示对话框肯定是副作用)都不应放在reducers中。将reducers视为被动的“状态构建器”。它们不会真正地“做”事情。 如果您希望显示警报,请在调度操作之前从组件中执行此操作,或者从操作创建者中执行此操作。调度操作时,执行副作用已经太迟了。 每个规则都有例外。有时,您的副作用逻辑非常复杂,您实际上想将它们与特定的操作类型或特定的reducers耦合。在这种情况下,请查看高级项目,例如Redux SagaRedux Loop。只有在熟悉vanilla Redux并且有散布的副作用问题需要更易于管理时才这样做。

17
如果我同时进行多个数据获取,那么一个变量就不够用了。 - philk
1
@philk 如果你有多个fetch请求,你可以使用Promise.all将它们分组成一个单一的promise,然后为所有的fetch请求派发一个单一的action。否则,你必须在你的状态中维护多个isFetching变量。 - Sebastien Lorber
2
请仔细查看我链接的示例。有不止一个 isFetching 标志。它为每个正在获取的对象集合设置。您可以使用 reducer 组合来实现这一点。 - Dan Abramov
3
请注意,如果请求失败,且RECEIVE_POSTS从未被触发,加载标志将保持不变,除非您创建了某种超时来显示“加载错误”的消息。 - James111
2
@TomiS - 我明确地将我所有的isFetching属性从我使用的任何redux持久性中列入黑名单。 - duhseekoh
显示剩余6条评论

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的对象,而是有很多:每个子版面一个(在该示例中被获取)。 - Dan Abramov
2
哦,是的,我没有注意到那个。然而在我的情况下,我在状态中有一个全局的isFetching条目和一个缓存条目,其中存储了获取的数据,对于我的目的,我只关心是否发生了一些网络活动,不太在意具体是什么。 - Nuno Campos
4
是的!这取决于您是否想在 UI 中的一个或多个位置显示获取指示器。实际上,您可以结合这两种方法,在屏幕顶部有一个全局的“fetchCounter”来显示进度条,并针对列表和页面使用几个特定的“isFetching”标志。 - Dan Abramov
如果我有多个文件中的POST请求,我该如何设置isFetching状态以跟踪其当前状态? - user989988

13

我想添加一些内容。真实世界的例子使用 store 中的字段 isFetching 表示正在获取集合中的项。任何集合都可以泛化为一个 pagination reducer,可以连接到您的组件以跟踪状态并显示是否正在加载集合。

我遇到了这样的情况,我想获取一个不符合分页模式的特定实体的详细信息。我希望有一个表示从服务器获取详细信息的状态,但我也不想只为此创建一个 reducer。

为了解决这个问题,我添加了另一个通用 reducer,称为 fetching。它的工作方式类似于分页 reducer,其职责仅是监视一组操作,并使用配对的 [entity, isFetching] 生成新状态。这允许将 reducer 连接到任何组件,并知道应用程序当前是否正在获取数据,而不仅仅是针对集合而言。


2
谢谢你的回答!处理单个项目的加载及其状态很少被讨论! - Gilad Peleg
当我的一个组件依赖于另一个组件的操作时,一种快速且不太规范的方法是在 mapStateToProps 中将它们结合起来,例如:isFetching: posts.isFetching || comments.isFetching。现在,当其中任何一个组件正在更新时,您可以阻止用户与两个组件进行交互。 - Philip Murphy

5
我是唯一一个认为加载指示器不属于 Redux 存储的人吗?我的意思是,我认为它并不是应用程序状态的一部分。 我现在使用 Angular2,我的做法是有一个“Loading”服务,通过 RxJS BehaviourSubjects 暴露不同的加载指示器。我猜机制是相同的,只是我没有将信息存储在 Redux 中。加载服务的用户只需订阅他们想要监听的事件即可。我的 Redux action creators 在需要更改时调用 LoadingService。UX 组件订阅了暴露的 observables...

这就是为什么我喜欢 store 的想法,所有的操作都可以被轮询(ngrx 和 redux-logic),service 不是函数式的,而 redux-logic 是函数式的。不错的阅读材料 - srghma
24
嗨,我回来查看已经超过一年了,只是想说我非常错了。当然 UX 状态应该归属于应用状态。我当时怎么这么蠢呢? - Spock

5
我直到现在才看到这个问题,但由于没有被接受的答案,我想提供一种解决方法。我写了一个工具来完成这个任务:react-loader-factory。它比Abramov的解决方案稍微复杂一些,但更加模块化和方便,因为我不想在编写后再去思考。
主要有四个部分:
- 工厂模式:这允许您快速调用相同的函数来设置哪些状态表示您的组件正在“加载”,以及要分派哪些操作。(这假设组件负责启动它等待的操作。)const loaderWrapper = loaderFactory(actionsList, monitoredStates); - 包装器:工厂生成的组件是一个“高阶组件”(就像Redux中connect()返回的那样),因此您可以将其添加到您现有的内容中。const LoadingChild = loaderWrapper(ChildComponent); - 操作/Reducer交互:包装器检查它插入的Reducer是否包含关键字,告诉它不要传递给需要数据的组件。包装器分派的操作预计会产生相关的关键字(例如redux-api-middleware分派ACTION_SUCCESSACTION_REQUEST)。 (当然,您也可以在其他地方分派操作并从包装器中进行监视。) - 加载指示器:当您的组件依赖的数据尚未准备好时,您希望出现的组件。我添加了一个小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 } />;
}

一个用于监视加载器的reducer(尽管如果您希望,可以以不同的方式连接它):

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. 你的其他直觉在我看来都是正确的。

3
您可以使用React Redux的connect()或低级别的store.subscribe()方法向您的存储添加更改侦听器。您应该在存储中拥有加载指示器,然后存储更改处理程序可以检查并更新组件状态。根据状态,组件将呈现预加载器,如果需要的话。 alertconfirm不应该是问题。它们是阻塞的,而且alert甚至不需要用户输入。对于confirm,如果用户选择应影响组件渲染,则可以基于用户单击设置状态。如果不是,则可以将选择存储为组件成员变量以供以后使用。

关于警告/确认代码,它们应该放在哪里,是在 actions 还是 reducers 中? - 企业应用架构模式大师
取决于您想要用它们做什么,但老实说,在大多数情况下,我会将它们放在组件代码中,因为它们是 UI 的一部分,而不是数据层。 - Miloš Rašić
一些UI组件通过触发事件(状态改变事件)来实现操作,而不是直接改变状态本身。例如动画、显示/隐藏预加载器等。您如何处理它们? - 企业应用架构模式大师
如果你想在React应用程序中使用非React组件,通常使用的解决方案是创建一个包装器React组件,然后使用其生命周期方法来初始化、更新和销毁非React组件的实例。大多数这样的组件在DOM中使用占位符元素进行初始化,你可以在React组件的render方法中呈现它们。你可以在这里阅读更多有关生命周期方法的信息:https://facebook.github.io/react/docs/component-specs.html - Miloš Rašić
我有一个情况:右上角有一个通知区域,其中包含一条通知消息,每个消息会在5秒后消失。此组件位于Webview之外,由宿主原生应用程序提供。它提供了一些js接口,例如 addNotification(message)。另外一个情况是预加载器,也由宿主原生应用程序提供,并通过其JavaScript API触发。我在React组件的componentDidUpdate中添加了一个包装器来处理这些API。我应该如何设计此组件的props或state呢? - 企业应用架构模式大师

3
我们的应用程序有三种类型的通知,都被设计为方面:
  1. 加载指示器(基于属性的模态或非模态)
  2. 错误弹出窗口(模态)
  3. 通知 Snackbar(非模态,自动关闭)
这三个组件都在我们的应用程序顶层(Main)中,并通过 Redux 进行数据交互控制。这些属性控制其相应方面的显示。
我设计了一个代理来处理我们所有的 API 调用,因此所有的 isFetching 和 (api) 错误都通过我导入到代理中的 actionCreators 进行调解。(顺便提一下,我还使用 webpack 注入一个支持开发的后台服务的模拟,以便我们可以不依赖服务器进行工作。)
应用程序中的任何其他位置,如果需要提供任何类型的通知,只需导入相应的 action。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组件{

我正在处理类似的设置,需要显示加载器/通知。但是我遇到了问题,你有没有相关的代码片段或示例可以分享一下,看看你是如何完成这些任务的? - Aymen

2

我正在保存类似这样的网址:

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

然后,我有一个通过reselect记忆化的选择器。

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

为了使 POST 的 URL 独特,我会将一些变量作为查询参数传递。
而在需要显示指示器的地方,我只需使用 getFetchCount 变量。

1
顺便说一下,您可以将 Object.keys(items).filter(item => items[item] === true).length > 0 ? true : false 替换为 Object.keys(items).every(item => items[item]) - Alexandre Annic
1
我认为你的意思是使用 some 而不是 every,但是第一个提议中有太多不必要的比较。Object.entries(items).some(([url, fetching]) => fetching); - Rafael Porras Lucena

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接