使用 Redux + React Router 的 React App 中的异步数据流?

Posted

技术标签:

【中文标题】使用 Redux + React Router 的 React App 中的异步数据流?【英文标题】:Async data flow in React App with Redux + ReactRouter? 【发布时间】:2016-03-27 08:50:47 【问题描述】:

我正在使用Redux 和React-router 制作一个简单的邮件应用程序。由于我对 Redux 比较陌生,所以我不太了解 Redux + Router 中的实际数据流。

我想要得到什么

    页面启动后 (/),MailListComponent 从服务器获取消息数组。此时 MessageComponent 没有显示,因为它没有一条消息可以为其获取数据。 获取state.messages:[] 后,应用导航到state.messages:[] (/messages/1) 的第一条消息。 转换完成后,MessageComponent 会显示并获取 id=1 信息的消息,它在单独的请求中作为附件。

这是组件模型:

我在做什么

// MailListActions.js
export function loadMessages() 
  return 
    type:    'LOAD_MESSAGES',
    promise: client => client.get('/messages')
  ;


// MailListReducer.js
import Immutable from 'immutable';

const defaultState =  messages: [], fetchingMessages: false ;

export default function mailListReducer(state = defaultState, action = ) 
  switch (action.type) 
    case 'LOAD_MESSAGES_REQUEST':
      return state.merge(fetchingMessages: true);

    case 'LOAD_MESSAGES':
        return state.merge(fetchingMessages: false, messages: action.res.data || null);

    case 'LOAD_MESSAGES_FAILURE':
        // also do something

    default:
      return state;
  

当我使用 promiseMiddleware、LOAD_MESSAGESLOAD_MESSAGES_REQUESTLOAD_MESSAGES_FAILURE 时,请求 /messages 结束时被调度。

现在:

    可以在 MailListComponent 的 componentDidMount 中 dispatch loadMessages() 吗? 应该如何正确转换为/messages/1? 我应该在我的州创建activeMessageId<Integer> 吗? 所有这些组件应该如何与 React-Router 连接?

这是我目前的尝试:

export default (store) => 
  const loadAuth = (nextState, replaceState, next) =>  ... ;

  return (
    <Route name="app" component=App path="/" onEnter=loadAuth>
      <IndexRoute component=Content/> // <== THIS IS A DUMMY COMPONENT. It diplays pre-loader until the app is transitioned to real first message
      <Route path="messages/:id" component=Message/>
    </Route>
  );
;

您能否提供一些要点,如何连接这些点? poper异步数据流逻辑是什么?

我使用isomorphic-redux 示例作为我的应用程序的基础。虽然是同构的,但和普通的 Redux 应用应该差别不大

谢谢。

更新

其中一个想法 - 为&lt;IndexRoute component=Content/&gt; 设置 onEnter 钩子,它将获取消息,设置为状态和初始转换。是redux+router的方式吗?

但是,这种方式也可能相当棘手,因为/messages 仅适用于经过身份验证的用户(其中​​store.getState().auth.get('loaded') == true

【问题讨论】:

我在下面回答了一个对我们来说效果很好的解决方案(非常相似的情况/功能)。我省略了大部分与 redux 操作/reducer 相关的代码,因为您上面解释的内容很好地涵盖了这一点。简短的版本是:组件生命周期对我们来说运行良好。这会导致在访问“messages/”时延迟加载消息、直接导航到消息等......我很想知道你在哪里登陆! 【参考方案1】:

在我看来,服务器端渲染很重要。没有它,您将提供仅在客户端生效的空白页面。它将严重影响您的 SEO。因此,如果我们认为服务器端渲染很重要,我们需要一种方法来获取适合服务器端渲染的数据。

查看the docs for server side rendering i.c.w. react-router,我们发现:

首先我们调用match,将当前位置和我们的路线传递给它 然后我们调用ReactDOMServer.render,传递我们从match 得到的renderProps

很明显,我们需要在进入渲染阶段之前访问获取的数据。

这意味着我们不能使用组件生命周期。我们也不能使用onEnter 或任何其他仅在渲染已经开始时触发的钩子。在服务器端,我们需要在渲染开始之前获取数据。这意味着我们需要能够确定从renderProps 获取什么,我们从match 获得。

常见的解决方案是在顶层组件上放置一个静态的fetchData 函数。在您的情况下,它可能看起来像这样:

export default class MailListComponent extends React.Component 
  static fetchData = (store, props) => 
    return store.dispatch(loadMessages());
  ;
  // ....

我们可以在服务器端找到这个fetchData 函数并在继续渲染之前在那里调用它,因为match 为我们提供了包含匹配组件类的renderProps。所以我们可以遍历它们并获取所有fetchData 函数并调用它们。像这样的:

var fetchingComponents = renderProps.components
  // if you use react-redux, your components will be wrapped, unwrap them
  .map(component => component.WrappedComponent ? component.WrappedComponent : component)
  // now grab the fetchData functions from all (unwrapped) components that have it
  .filter(component => component.fetchData);

// Call the fetchData functions and collect the promises they return
var fetchPromises = fetchingComponents.map(component => component.fetchData(store, renderProps));

fetchData 返回store.dispatch 的结果,即Promise。 在客户端,这只会显示一些 loading 屏幕,直到 Promise 完成,但在服务器端,我们需要 等待 直到发生这种情况,所以当我们在存储中实际有数据时进入渲染阶段。我们可以使用Promise.all

// From the components from the matched route, get the fetchData functions
Promise.all(fetchPromises)
  // Promise.all combines all the promises into one
  .then(() => 
    // now fetchData() has been run on every component in the route, and the
    // promises resolved, so we know the redux state is populated
    res.status(200);
    res.send('<!DOCTYPE html>\n' +
      ReactDOM.renderToString(
        <Html lang="en-US" store=app.store ...renderProps script="/assets/bridalapp-ui.js" />
      )
    );
    res.end();
)

给你。我们向客户端发送一个完全填充的页面。在那里,我们可以使用onEnter 或生命周期挂钩或任何其他方便的方法来获取用户在客户端导航时所需的后续数据。但是我们应该尽量确保我们在组件本身上有一个可用的函数或注释(初始操作?),以便我们可以预先获取数据以进行服务器端渲染。

【讨论】:

我只想指出这是一个邮件应用程序。这意味着它将明确可供搜索引擎访问。所以SEO是一个有争议的问题。如果客户端无论如何都会渲染 React 组件......为什么要为他们提供预渲染页面? 但是您对服务器渲染提出了一个很好的观点。做对了,对于简单的屏幕,你可以让页面在没有 JS 的情况下工作。我刚刚想到它可能会提供一些初始性能提升。 @FighterJet 事实上,许多 s-s-r 倡导者认为最初的性能提升比 SEO 优势更重要。参见例如openmymind.net/2012/5/30/Client-Side-vs-Server-Side-Rendering【参考方案2】:

我一直在开发一个相当大的应用程序(React、Redux、React Router 等...),它具有非常相似的功能(带有侧边栏 + 搜索栏/工具的消息浏览器等...)它是 结构上几乎相同与您在上面列出的内容。使用React's component lifecycle 对我们来说效果很好。

基本上由组件来决定,“给定这些数据(消息、加载等...),我应该是什么样子和/或做什么?”。

我们一开始就搞乱了 onEnter 和其他“组件外”策略,但他们开始觉得它们过于复杂。同样相关的是您关于存储activeMessageId 的问题。如果我正确理解您的情况,这应该可靠地从示例中的当前路线params.id 推导出来。

为了让我们了解这种方法正在为我们完成的一些事情

当然,这个示例被精简/简化了很多,但它总结了“请求消息”部分,并且非常接近于我们的实际方法。

const MailApp = React.createClass(
  componentWillMount() 
    this._requestIfNeeded(this.props);
  ,

  componentWillUpdate(newProps) 
    this._requestIfNeeded(newProps);
  ,

  _requestIfNeeded(props) 
    const 
      // Currently loaded messages
      messages,

      // From our route "messages/:id" (ReactRouter)
      params: id,

      // These are our action creators passed down from `connect`
      requestMessage,
      requestMessages,

      // Are we "loading"?
      loading,
       = props;

    if (!id) 
      // "messages/"
      if (messages.length === 0 && !loading)
        return requestMessages();
     else 
      // "messages/:id"
      const haveMessage = _.find(messages, id);
      if (!haveMessage && !loading)
        return requestMessage(id);
    
  ,

  render() 
    const 
      messages,
      params: id,
     = props;

    return (
      <div>
        <NavBar />
        <Message message=_.find(messages, id)/>
        <MailList message=messages />
      </div>
    )
  
);

我很想知道这是否对您有帮助,或者您是否在其他地方登陆。我已经看到围绕这些主题出现了类似的问题,并且会对您发现的内容感兴趣。

【讨论】:

【参考方案3】:

React-Router 2 API 通过让您在路由器和应用程序之间建立一个层,开辟了有趣的数据获取可能性。这是一个附加插件的好地方,比如 https://github.com/rackt/async-props 或 https://github.com/raisemarketplace/ground-control 。

GroundControl 中的通用 fetchData 挂钩(特定于 Redux)为您提供了相当多的功能。让您阻止组件渲染客户端;处理异步渲染;然后派发到你的减速机。

const fetchData = (done,  dispatch, clientRender ) => 
  // show blocking loading component
  setTimeout(() => 
    clientRender(); // non-blocking loader (props.loading)
    setTimeout(() => 
      dispatch(actions.load( mydata );
      done();
    , 1000);
  , 1000)
;

在路由上声明减速器,并调用每个嵌套路由的 fetchData 函数。因此,您可以让父布局路由句柄身份验证。然后fetchData 在嵌套路由中处理他们关心的事情。

【讨论】:

以上是关于使用 Redux + React Router 的 React App 中的异步数据流?的主要内容,如果未能解决你的问题,请参考以下文章

混淆 react-router-dom 和 react-router-redux

React + Redux + Router - 我应该为所有页面/组件使用一个状态/存储吗?

Redux Sagas 未使用 redux persist 和 connected-react-router 进入

Redux 中的 React Router 路由参数

如何用 redux 和 react-router 组织状态?

使用 Redux + React Router 的 React App 中的异步数据流?