【问题标题】:(Universal React + redux + react-router) How to avoid re-fetching route data on initial browser load?(通用 React + redux + react-router)如何避免在初始浏览器加载时重新获取路由数据?
【发布时间】:2016-07-13 20:36:42
【问题描述】:

我在我的 Route 组件上使用 static fetchData 方法...

const mapStateToProps = (state) => ({
  posts: state.posts
})

@connect(mapStateToProps)
class Blog extends Component {

  static fetchData (dispatch) {
    return dispatch(fetchPosts())
  }

  render () {
    return (
      <PostsList posts={this.props.posts} />
    )
  }

}

...并在服务器端初始渲染之前收集所有承诺...

match({ routes, location }, (error, redirectLocation, renderProps) => {
    const promises = renderProps.components
      .filter((component) => component.fetchData)
      .map((component) => component.fetchData(store.dispatch))

    Promise.all(promises).then(() => {
      res.status(200).send(renderView())
    })
})

它工作正常,服务器等到我的所有承诺都得到解决后再渲染应用程序。

现在,在我的客户端脚本上,我正在执行与服务器上类似的操作...

...
function resolveRoute (props) {
  props.components
    .filter((component) => component.fetchData)
    .map((component) => component.fetchData(store.dispatch))

  return <RouterContext {...props} />
}

render((
  <Provider store={store}>
    <Router
      history={browserHistory}
      routes={routes}
      render={resolveRoute} />
  </Provider>
), document.querySelector('#app'))

而且效果很好。但是,正如您可能推断的那样,在初始页面呈现时,静态 fetchData 被调用了两次(一次在服务器上,一次在客户端上),我不希望这样。

关于如何解决这个问题有什么建议吗?推荐?

【问题讨论】:

    标签: reactjs react-router redux redux-thunk


    【解决方案1】:

    我是用手机输入的,所以对于格式缺失表示歉意。

    对于我的项目,我正在做与您类似的事情;我有一个静态的 fetchData 方法,我遍历来自 renderProps 的组件,然后调用静态方法并等待 Promise 解决。

    然后,我从我的 redux 存储中调用 get state,对其进行字符串化,并将其传递给服务器上的渲染函数,以便它可以在客户端渲染出初始状态对象。

    我只是从客户端获取初始状态变量并将其传递给我的 redux 存储。然后 Redux 将处理让您的客户端存储与服务器上的存储相匹配。从那里,您只需将商店传递给提供商并照常继续。您根本不需要在客户端上调用静态方法。

    作为我所说的示例,您可以查看我的 github 项目,因为代码会自行解释。 https://github.com/mr-antivirus/riur

    希望有所帮助!


    [编辑] 这是代码!

    Client.js

    'use strict'
    
    import React from 'react';
    import { render } from 'react-dom';
    import { Provider } from 'react-redux';
    import { Router, browserHistory } from 'react-router';
    import createStore from '../shared/store/createStore';
    
    import routes from '../shared/routes';
    
    const store = createStore(window.__app_data);
    const history = browserHistory;
    
    render (
        <Provider store={store}>
            <Router history={history} routes={routes} />
        </Provider>,
        document.getElementById('content')
    )
    

    Server.js

    app.use((req, res, next) => {
        match({ routes, location:req.url }, (err, redirectLocation, renderProps) => {
            if (err) {
                return res.status(500).send(err);
            }
    
            if (redirectLocation) {
                return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
            }
    
            if (!renderProps) {
                return next();
            }
    
            // Create the redux store.
            const store = createStore();
    
            // Retrieve the promises from React Router components that have a fetchData method.
            //  We use this data to populate our store for server side rendering.
            const fetchedData = renderProps.components
                .filter(component => component.fetchData)
                .map(component => component.fetchData(store, renderProps.params));
    
            // Wait until ALL promises are successful before rendering.
            Promise.all(fetchedData)
                .then(() => {
                    const asset = {
                        javascript: {
                            main: '/js/bundle.js'
                        }
                    };
    
                    const appContent = renderToString(
                        <Provider store={store}>
                            <RouterContext {...renderProps} />
                        </Provider>
                    ) 
    
                    const isProd = process.env.NODE_ENV !== 'production' ? false : true;
    
                    res.send('<!doctype html>' + renderToStaticMarkup(<Html assets={asset} content={appContent} store={store} isProd={isProd} />));
                })
                .catch((err) => {
                    // TODO: Perform better error logging.
                    console.log(err);
                });
        });
    }); 
    

    RedditContainer.js

    class Reddit extends Component {
        // Used by the server, ONLY, to fetch data 
        static fetchData(store) {
            const { selectedSubreddit } = store.getState();
            return store.dispatch(fetchPosts(selectedSubreddit));
        }
    
        // This will be called once on the client
        componentDidMount() {
            const { dispatch, selectedSubreddit } = this.props;
            dispatch(fetchPostsIfNeeded(selectedSubreddit));
        }
    
        ... Other methods
    };
    

    HTML.js

    'use strict';
    
    import React, { Component, PropTypes } from 'react';
    import ReactDom from 'react-dom';
    import Helmet from 'react-helmet';
    import serialize from 'serialize-javascript';
    
    export default class Layout extends Component {
        static propTypes = {
            assets: PropTypes.object,
            content: PropTypes.string,
            store: PropTypes.object,
            isProd: PropTypes.bool
        }
    
        render () {
            const { assets, content, store, isProd } = this.props;
            const head = Helmet.rewind();
            const attrs = head.htmlAttributes.toComponent();
    
            return (
                <html {...attrs}>
                    <head>
                        {head.base.toComponent()}
                        {head.title.toComponent()}
                        {head.meta.toComponent()}
                        {head.link.toComponent()}
                        {head.script.toComponent()}
    
                        <link rel='shortcut icon' href='/favicon.ico' />
                        <meta name='viewport' content='width=device-width, initial-scale=1' />
                    </head>
                    <body>
                        <div id='content' dangerouslySetInnerHTML={{__html: content}} />
                        <script dangerouslySetInnerHTML={{__html: `window.__app_data=${serialize(store.getState())}; window.__isProduction=${isProd}`}} charSet='utf-8' />
                        <script src={assets.javascript.main} charSet='utf-8' />
                    </body>
                </html>
            );
        }
    };
    

    重申...

    1. 在客户端,获取状态变量并将其传递给您的商店。
    2. 在服务器上,循环访问调用 fetchData 并传递存储的组件。等待 promise 被解析,然后渲染。
    3. 在 HTML.js(您的 renderView 函数)中,序列化您的 Redux 存储并将输出呈现到客户端的 javascript 变量中。
    4. 在您的 React 组件中,为 服务器创建一个静态 fetchData 方法来调用。调度您需要的操作。

    【讨论】:

    • 谢谢!我遇到了与 OP 相同的问题,但第 4 步是啊哈,只有在客户端挂载时才需要获取
    • 很高兴它有帮助:-)
    【解决方案2】:

    更好的办法是在服务器端对存储状态进行脱水,并在客户端将初始存储状态与脱水状态混合。

    来自 Redux 文档:

    这使得创建通用应用程序变得容易,因为来自服务器的状态可以被序列化并混合到客户端中,而无需额外的编码工作。

    http://redux.js.org/docs/introduction/ThreePrinciples.html

    【讨论】:

      【解决方案3】:

      您可以使用来自 fbjs 模块的 CanUseDOM。

      import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
      //only render on the server because it doesn't have DOM
      if(!canUseDOM)
       static fetch here
      

      【讨论】:

      • 但是,使用这种方式我不会失去服务器端的承诺解析吗?
      猜你喜欢
      • 2017-09-28
      • 1970-01-01
      • 1970-01-01
      • 2018-01-05
      • 1970-01-01
      • 1970-01-01
      • 2016-02-04
      • 1970-01-01
      • 2016-12-23
      相关资源
      最近更新 更多