Solution to Meteor React Code Splitting + SSR


#1

Code Splitting + SSR with React Router 4 is a tricky thing in Meteor architecture because it doesn’t support Webpack. Recently I wrote a library to solve this issue.

Server side: Load all components synchronously, then render to string.
Client side: Load the initial component before rendering, render the entire screen synchronously, then load the rest of routes asynchronously.

This is the repo for react-code-split-ssr. There is also a link of example in it.

This package can solve 90% of code splitting + SSR issue. Let’s think further. In Meteor server-render package, there’s no way to do redirection or get cookies for login token from request header. I don’t know if MDG will extend server-render package, but currently the only way seems to use WebApp.connectHandlers for the access of req object from client. What do you think?


#2

Great work @lhz516, thanks for sharing, I’ll give it a try soon to understand it’s behavior (what exactly is loaded when). I’m still on React Router v3, so this will also help me to update.

In Meteor server-render package, there’s no way to do redirection or get cookies for login token from request header. I don’t know if MDG will extend server-render package, but currently the only way seems to use WebApp.connectHandlers for the access of req object from client

I’ve used fast-render auth file to inject the cookie in the request header. Then used the following block of code to retrieve the user object (again from the fast-render package)


// A workaround to pass the request and cookie info
WebApp.connectHandlers.use('/', (req, res, next) => {
  const loginToken = req.cookies.meteor_login_token;
  if (req.url === '/' && loginToken) {
    if (loginToken) {
      const hashedToken = loginToken && Accounts._hashLoginToken(loginToken);
      const query = {
        'services.resume.loginTokens.hashedToken': hashedToken
      };
      const options = { fields: { _id: 1 } };
      user = Meteor.users.findOne(query, options);
      loggedIn = true;
    }
  } else {
    loggedIn = false;
    user = null;
  }
  return next();
});

Then at the onPageLoad function I’d check the user and loggedIn variables. It’s working for now, but not sure if this valid for multiple concurrent requests. Ideally we’d have the server render package extended to support this use case, I think you’ve opened a github issue on this.

At the client I’ve used React Loadable to asynchronously load module

import React from 'react';
import Loadable from 'react-loadable';

const AsyncModule = Loadable({
  loader: () => import('./module.js').then(ex => ex.default),
  loading: () => <div />
});

export default AsyncModule;

And at the router:

<Route name="module" path="/module" component={AsyncModule} />

I only SSR the landing page, login page and the home page for returning visitors and dynamically import everything else.

For the initial data, I inject them with the initial server rendered page using the sink.appendToBody function.

     const encodedData = encodeURIComponent(JSON.stringify(someUserDataArray));
     sink.appendToBody(
        `<script type="text/injected-data" id='injected-date'}>${encodedData}</script>`
      );

and then I use the following code to retrieve them at the client and pass them to the react component.

 const injectedData = document.getElementById('injected-date');
    if (injectedData && limit.get() <= DEFAULT_LIMIT) {
      const data = injectedData.innerHTML;
      if (data) {
        const injectedPosts = JSON.parse(decodeURIComponent(data));
        injectedData.remove();
        ...

I really think there is a need for a package that make implementing those capabilities easier and your package seems like a good starting point!


#3

@alawi Thanks for your long reply.

Since react-code-split-ssr is an npm package, there won’t be any data injection based on Meteor, it’s just a plugin for router.

I have tried react-loadable. However there’s an issue when doing ssr + code splitting. When you are entering the page, server renders the full page, after the client JS bundle is loaded, it will flash a loading screen, then the async module will be loaded. Like this:

Full page -> Loading -> Full page

opts.delay couldn’t fix it.

For the login token access, I have tested this way from meteor#8977. It works for multiple concurrent requests although it looks weird.

WebApp.rawConnectHandlers.use(cookieParser());
// A workaround to pass the cookies info to the request sink object
WebApp.connectHandlers.use('/', (req, res, next) => {
  if (req.cookies.meteor_login_token) {
    req.dynamicBody = '<span id="signedIn"/>'; // also put login token here
  }
  return next();
});

// A workaround to detect if the user has signed on
const isSignedIn = sink => {
  if (!sink || !sink.request || !sink.request.dynamicBody) return false;
  return sink.request.dynamicBody === '<span id="signedIn"/>';
};

#4

My apology for hijacking your announcement, just wanted share what I know in case it helps and I got really excited about your package since I’ve been wrestling with this stuff myself :slight_smile:

Fair enough, I was just hinting for the need of a package to manage the data inspired by fast-render, I don’t think routing is sufficient for complete SSR solution.

Yeah I’ve this issue, hows does your solution solves for this because the loading is when the component is being fetched, does the router wait until the module is loaded before switching?


#5

For the complete solution, I prefer not to use a big package because it would be hard to customize. That’s why I don’t like to put too much stuff in one package. What I’m looking for is an architecture which is stable, extendable and readable. It can be a combination of several packages.

As what I mentioned in the topic. Load the initial component before rendering routes, so there won’t be a loading screen when first time entering.