Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Server Side React Initial Render causes duplicated async calls

I'm using react-router 2.0 on the server. Some of my top-level route components depend on async data to be fetched and uses Redux to manage state. To deal with this, I'm using a static fetchData() method to return a Promise.all of async actions as suggested in the official Redux docs. It works pretty well - the initial render from the server comes with the appropriate data in the Redux store.

My issue comes from how to write these async calls in a way that will work on both the initial render on the server and subsequent renders on the client as different routes get loaded. Currently, I make calls to the static fetchData() method in my top-level component's componentDidMount(), but this would cause the fetchData() to be called once the component mounts on the client. This is what I want when navigating to the route from another, but not on the initial render as we already made this call on the server.

I've been struggling with writing my data fetching logic in a way that:

  1. Only gets called once on initial render on the server, not on the initial client render
  2. Will render when a user navigates in from another route.

I was thinking of injecting a prop like { serverRendered: true } into the initial component but I only have access to the <RouterContext> when calling renderToString(). I took a peek at the source for but it doesn’t seem like I can pass the property from that JSX tag either.

I guess my questions would be:

  1. Is there a way to inject a property in <RouterContext> so that it passes down to the 'parent' top-level route component? If not in <RouterContext>, is there another way to inject the property in?
  2. If #1 isn't possible, is there a better way to handle fetchData() so the initial render won't cause fetchData() to be called on both the server and client?

Here’s a code sample of my server.jsx, the fetchData() handling middleware and an example top-level compnent.

like image 363
daleee Avatar asked Feb 11 '16 14:02

daleee


1 Answers

I faced up with the same issue. Clearing some global value with setTimeout or checking Redux store for data existence didn't look good for me.

I decided to check on which paths initial data was loaded, put this array as global value and check matches for route on componentDidMount.

My server index.js

const pathsWithLoadedData= []; // here I store paths

const promises = matchRoutes(Routes, req.path)
    .map(({ route }) => {
        if (route.loadData) {
            // if route loads data and has path then save it
            route.path && pathsWithLoadedData.push(route.path);
            return route.loadData(store);
        } else {
            return Promise.resolve(null);
        }
    })

My render template

<body>
   ...
   <script> 
      window.INITIALLY_LOADED_PATHS = ${JSON.stringify(pathsWithLoadedData)};
   </script>
   ...   
</body>

Some componentDidMount function

componentDidMount () {
    if (!wasFetchedInitially(this.props.route.path)) {
        this.props.fetchUsers();
    }
}

And helper

export const wasFetchedInitially = componentPath => {
    const pathIndex = window.INITIALLY_LOADED_PATHS.indexOf(componentPath);
    if (pathIndex < 0) return false;

    window.INITIALLY_LOADED_PATHS.splice(pathIndex, 1);
    return true;
};
like image 196
MaruniakS Avatar answered Sep 23 '22 14:09

MaruniakS