Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

(Universal React + redux + react-router) How to avoid re-fetching route data on initial browser load?

I am using a static fetchData method on my Route component...

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} />
    )
  }

}

... and collecting all promises before the initial render on the server side...

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())
    })
})

It works fine, the server waits until all my promises are resolved before rendering app.

Now, on my client script, I am doing something similar as on the server...

...
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'))

And it works fine. But, as you may deduct, on the initial page render, the static fetchData is getting called twice (once on the server and once on the client), and I don't want that.

Is there any suggestions on how to solve this? Recommendations?

like image 445
kevinwolf Avatar asked Mar 26 '16 02:03

kevinwolf


People also ask

How do I restrict routes in react router?

The Route component from react-router is public by default but we can build upon it to make it restricted. We can add a restricted prop with a default value of false and use the condition if the user is authenticated and the route is restricted, then we redirect the user back to the Dashboard component.

Which API from react router is used to create a navigation link and can avoid reloading of page?

react-router-dom allows us to navigate through different pages on our app with/without refreshing the entire component. By default, BrowserRouter in react-router-dom will not refresh the entire page.

What is difference between BrowserRouter and HashRouter?

HashRouter: When we have small client side applications which doesn't need backend we can use HashRouter because when we use hashes in the URL/location bar browser doesn't make a server request. BrowserRouter: When we have big production-ready applications which serve backend, it is recommended to use <BrowserRouter> .


1 Answers

I'm typing this from my phone, so I apologize for the lack of formatting.

For my project, I'm doing something similar to you; I have a static fetchData method, I loop through the components from renderProps and then I call the static method and wait for the promises to resolve.

I then, call get state from my redux store, stringify it, and pass it to my render function on the server so that it can render out an initial state object on the client.

From the client, I just grab that inital state variable and pass it to my redux store. Redux will then handle getting your client store to match the one on the server. From there, you just pass your store to the provider and go on as usual. You shouldn't need to call your static method on the client at all.

For an example of what I said, you can check out my github project as code explains itself. https://github.com/mr-antivirus/riur

Hope that helped!


[Edit] Here is the code!

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

To reiterate...

  1. On the client, grab the state variable and pass it to your store.
  2. On the server, loop through your components calling fetchData and passing your store. Wait for the promises to be resolved, then render.
  3. In HTML.js (Your renderView function), serialize your Redux store and render the output to a javascript variable for the client.
  4. In your React component, create a static fetchData method for ONLY the server to call. Dispatch the actions you need.
like image 56
Mr_Antivius Avatar answered Sep 29 '22 17:09

Mr_Antivius