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

Posted

技术标签:

【中文标题】获取数据时如何在 React Redux 应用程序中显示加载指示器? [关闭]【英文标题】:How to show a loading indicator in React Redux app while fetching the data? [closed] 【发布时间】:2016-05-29 04:06:16 【问题描述】:

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

正如主页 (Lifecycle) 所说,获取 API 生命周期以调度 CALL_API 动作开始,以调度 FSA 动作结束。

所以我的第一个案例是在获取 API 时显示/隐藏预加载器。中间件将在开始时分派一个 FSA 动作并在结束时分派一个 FSA 动作。这两个动作都被减速器接收,它们应该只做一些正常的数据处理。没有 UI 操作,没有更多的操作。也许我应该将处理状态保存在状态中,然后在商店更新时呈现它们。

但是怎么做呢?一个反应组件流过整个页面?从其他操作更新商店会发生什么?我的意思是它们更像是事件而不是状态!

即使是更糟糕的情况,当我必须在 redux/react 应用程序中使用本机确认对话框或警报对话框时,我该怎么办?它们应该放在哪里,actions 还是 reducers?

祝你好运!希望回复。

【问题讨论】:

回滚了对这个问题的最后一次编辑,因为它改变了下面问题和答案的全部要点。 一个事件就是状态的改变! 看看 questrar。 github.com/orar/questrar 【参考方案1】:

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

alertconfirm 应该不是问题。它们处于阻塞状态,警报甚至不接受用户的任何输入。使用confirm,如果用户选择会影响组件渲染,您可以根据用户单击的内容设置状态。如果没有,您可以将选择存储为组件成员变量以供以后使用。

【讨论】:

关于警报/确认代码,它们应该放在哪里,操作或减速器? 取决于你想用它们做什么,但老实说,在大多数情况下我会将它们放在组件代码中,因为它们是 UI 的一部分,而不是数据层的一部分。 一些 UI 组件通过触发事件(改变状态的事件)而不是状态本身来起作用。例如动画,显示/隐藏预加载器。你如何处理它们? 如果你想在你的 react 应用程序中使用非反应组件,通常使用的解决方案是制作一个包装反应组件,然后使用其生命周期方法来初始化、更新和销毁非反应组件的实例。反应组件。大多数此类组件使用 DOM 中的占位符元素进行初始化,您将在 react 组件的 render 方法中渲染这些元素。您可以在此处阅读有关生命周期方法的更多信息:facebook.github.io/react/docs/component-specs.html 我有一个案例:右上角有一个通知区域,里面有一条通知消息,每条消息显示5秒后消失。这个组件在 webview 之外,由宿主本机应用程序提供。它提供了一些js接口,例如addNofication(message)。另一种情况是预加载器,它也由宿主本机应用程序提供并由其 javascript API 触发。我在 React 组件的componentDidUpdate 中为这些 api 添加了一个包装器。如何设计这个组件的 props 或 state?【参考方案2】:

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

我不会这么说。我认为加载指示器是 UI 的一个很好的例子,它很容易被描述为状态的函数:在这种情况下,是一个布尔变量。虽然this answer 是正确的,但我想提供一些代码来配合它。

async example in Redux repo,减速机updates a field called 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 的状态和 returns isFetching as part of the mapStateToProps() return value,因此它可以在连接组件的 props 中使用:

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

  return 
    selectedReddit,
    posts,
    isFetching,
    lastUpdated
  

最后,组件uses isFetching prop in the render() function 呈现“正在加载...”标签(可以想象,它可能是一个微调器):

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

即使是更糟糕的情况,当我必须在 redux/react 应用程序中使用本机确认对话框或警报对话框时,我该怎么办?它们应该放在哪里,actions 还是 reducers?

任何副作用(显示对话框肯定是副作用)不属于 reducer。将减速器视为被动的“状态建设者”。他们并没有真正“做”事情。

如果您希望显示警报,请在调度操作之前从组件执行此操作,或者从操作创建者执行此操作。当一个动作被调度时,为了响应它而执行副作用为时已晚。

对于每条规则,都有一个例外。有时您的副作用逻辑非常复杂,您实际上想要将它们耦合到特定的操作类型或特定的减速器。在这种情况下,请查看像 Redux Saga 和 Redux Loop 这样的高级项目。仅当您对 vanilla Redux 感到满意并且遇到真正的分散副作用问题时才这样做,您希望使其更易于管理。

【讨论】:

如果我要进行多次提取怎么办?那么一个变量是不够的。 @philk 如果您有多个提取,您可以将它们与Promise.all 组合成一个承诺,然后为所有提取分派一个操作。或者您必须在您的状态中维护多个 isFetching 变量。 请仔细查看我链接到的示例。有多个isFetching 标志。它是为要获取的每组对象设置的。您可以使用 reducer 组合来实现它。 请注意,如果请求失败,并且永远不会触发 RECEIVE_POSTS,加载标志将保持原位,除非您创建了某种超时来显示 error loading 消息。 @TomiS - 我明确地将我所有的 isFetching 属性从我正在使用的任何 redux 持久性中列入黑名单。【参考方案3】:

我想补充一点。真实世界的示例使用商店中的字段isFetching 来表示何时获取项目的集合。任何集合都被泛化为 pagination 减速器,可以连接到您的组件以跟踪状态并显示集合是否正在加载。

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

为了解决这个问题,我添加了另一个名为 fetching 的通用减速器。它的工作方式与分页缩减器类似,它的职责只是观察 一组动作并使用对[entity, isFetching] 生成新状态。这允许 connect 将 reducer 发送到任何组件,并知道应用当前是否正在获取数据,而不仅仅是针对集合,而是针对特定实体。

【讨论】:

感谢您的回答!很少讨论处理单个项目的加载及其状态! 当我有一个组件依赖于另一个组件的操作时,一个快速而肮脏的出路是在你的 mapStateToProps 中像这样组合它们:isFetching:posts.isFetching || cmets.isFetching - 现在您可以在任一组件更新时阻止用户交互。【参考方案4】:

丹·阿布拉莫夫的回答很好! 只是想补充一点,我在我的一个应用程序中或多或少地这样做(将 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

【讨论】:

这是合理的。然而,大多数情况下,除了标志之外,您还希望存储一些您获取的 data。此时,您需要拥有多个带有isFetching 标志的对象。如果您仔细查看我链接到的示例,您会发现没有一个带有isFetched 的对象,而是有很多:每个 subreddit 一个(这是该示例中获取的内容)。 哦。是的,我没有注意到。但是在我的情况下,我在状态中有一个全局 isFetching 条目和一个存储所获取数据的缓存条目,出于我的目的,我只真正关心某些网络活动正在发生,这并不重要 是的!这取决于您是想在 UI 的一处还是多处显示抓取指示器。事实上,您可以将这两种方法结合起来,both 为屏幕顶部的某个进度条设置一个全局 fetchCounter,并为列表和页面设置几个特定的​​ isFetching 标志。 如果我在多个文件中有 POST 请求,我将如何设置 isFetching 的状态以跟踪其当前状态?【参考方案5】:

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

    加载指示器(基于 prop 的模态或非模态) 错误弹出窗口(模态) 通知快餐栏(非模态,自动关闭)

所有这三个都位于我们应用程序的顶层(主),并通过 Redux 连接,如下面的代码 sn-p 所示。这些道具控制着它们相应方面的显示。

我设计了一个代理来处理我们所有的 API 调用,因此所有 isFetching 和 (api) 错误都通过我在代理中导入的 actionCreators 进行调解。 (顺便说一句,我还使用 webpack 为 dev 注入了一个支持服务的模拟,这样我们就可以在没有服务器依赖的情况下工作。)

应用程序中需要提供任何类型通知的任何其他位置只需导入适当的操作即可。 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

【讨论】:

我正在使用类似的设置来显示加载器/通知。我遇到了问题;你有一个关于你如何完成这些任务的要点或例子吗?【参考方案6】:

直到现在我才想到这个问题,但由于没有人接受任何答案,我会扔掉我的帽子。我为这项工作编写了一个工具:react-loader-factory。它比 Abramov 的解决方案稍微多一些,但更加模块化和方便,因为我不想在写完之后再思考。

有四大块:

工厂模式:这允许您快速调用相同的函数来设置组件的哪些状态表示“正在加载”,以及要调度哪些操作。 (这假设组件负责启动它等待的操作。)const loaderWrapper = loaderFactory(actionsList, monitoredStates); Wrapper:Factory 生产的组件是一个“高阶组件”(就像 Redux 中 connect() 返回的一样),因此您可以将其固定到现有的东西上。 const LoadingChild = loaderWrapper(ChildComponent); Action/Reducer 交互:包装器检查它所插入的 reducer 是否包含告诉它不要传递给需要数据的组件的关键字。包装器分派的操作预计会产生相关的关键字(例如 redux-api-middleware 分派ACTION_SUCCESSACTION_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  />;

Loader 监控的 reducer(尽管您可以 wire it differently 如果您愿意):

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;

我希望在不久的将来我会在模块中添加超时和错误等内容,但模式不会有太大不同。


您的问题的简短答案是:

    将渲染与渲染代码联系起来——在您需要渲染的组件周围使用一个包装器,并使用我上面展示的数据进行渲染。 添加一个 reducer,使您可能关心的应用程序的请求状态易于理解,因此您不必过多考虑正在发生的事情。 事件和状态并没有真正的不同。 你的其他直觉对我来说似乎是正确的。

【讨论】:

【参考方案7】:

我是唯一一个认为加载指示器不属于 Redux 商店的人吗?我的意思是,我认为它本身不是应用程序状态的一部分。

现在,我使用 Angular2,我所做的是我有一个“加载”服务,它通过 RxJS BehaviourSubjects 公开不同的加载指标。我想机制是一样的,我只是不将信息存储在还原。

LoadingService 的用户只需订阅他们想听的那些事件..

每当需要更改时,我的 Redux 操作创建者都会调用 LoadingService。 UX 组件订阅暴露的 observables...

【讨论】:

这就是为什么我喜欢 store 的想法,所有的 action 都可以被轮询(ngrx 和 redux-logic),service 不是函数式的,redux-logic - 函数式的。 Nice reading 嗨,一年多后回来查看,只是说我错了。当然UX状态属于应用状态。我能有多傻?【参考方案8】:

我正在保存 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
);

为了在 POST 的情况下使 url 唯一,我将一些变量作为查询传递。

我想在哪里显示指标,我只是使用 getFetchCount 变量

【讨论】:

你可以顺便把Object.keys(items).filter(item =&gt; items[item] === true).length &gt; 0 ? true : false换成Object.keys(items).every(item =&gt; items[item]) 我认为您的意思是 some 而不是 every,但是是的,在第一个提议的解决方案中不需要进行太多比较。 Object.entries(items).some(([url, fetching]) =&gt; fetching);

以上是关于获取数据时如何在 React Redux 应用程序中显示加载指示器? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如何在 React hooks 和 redux 中只获取一次且不再获取数据

如何使用 Redux 获取新数据以响应 React Router 更改?

在 react-redux 应用程序中获取详细 api 数据的良好做法

React/Redux - 你啥时候应该从 API 获取新数据来更新你的商店?

React Redux - 删除对象的数据获取模式

React/Redux - 在应用程序加载/初始化时调度操作