Skip to content

Instantly share code, notes, and snippets.

@rishiraj824
Last active January 19, 2020 21:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rishiraj824/a9c0992460f27e5b1b75482dcc2c91ea to your computer and use it in GitHub Desktop.
Save rishiraj824/a9c0992460f27e5b1b75482dcc2c91ea to your computer and use it in GitHub Desktop.
Going about SSR in a CRA app.

Ideally the server should only be serving static files and to do that your express app should use some simple templating engine. This combined with a default loader in your HTML file should take care of the overall loading.

I have attached a prefetcher module below, a middleware, whose main purpose is to fetch the data required to render a route.

Important Steps

  1. You can have an index.marko or an index.html which will be your skeleton file. I have demonstrated it using marko.js, you can use any other templating engine.
<!-- Meta HTML/marko files -->
import meta from './commons/meta.marko';

<!doctype html>
<html>
<head>
    <include(meta, input)/>
    <!-- Styles will be inserted inline by build process -->
    <style>{{styles.css}}</style>
    
    <link rel="stylesheet" href="${input.assets.css.app}">
    <script type="text/javascript" src="${input.assets.js.vendor}"/>
    <script type="text/javascript" src="${input.assets.js.app}"/>

    
</head>
<body>
<!-- Your Spinner/Loader -->
<div class="app-container">
    <div class="page-content">
        <div id="content" class="content"></div>
        <div id="shell-spinner" class="overlay">
            <!--loader-->
        </div>
    </div>
</div>
</body>
</html>
  1. A Sample server/app.js for this process
// all imports
....
....

export default (app, ...) => {
/** all your other middlewares **/
/** all your routes **/
/** inject all your headers **/

app.get('/', (req, res, next) => {
    if (req._sessionData) {
      res.redirect(INDEX_URL);
    } else {
      // renderView is imported from the prefetcher.js, name is specified as 'login', check this function out 
      in renderer.js below in 4.
      renderView(res, {
        name: 'login',
        data: {
          assets: {
            js: {
              app: [path/to/the/js/file],
              vendor: [path/to/the/vendor/js/file],
            },
          },
        },
        //dataPrefetcher (Prefetcher File in 3.) will fetch all the data before the route renders and till then your index.marko file will be shown (loader)
        dataProvider: dataPrefetcher(req),
      });
    }
  });
  return app
}
  1. Prefetcher File
const fetchers = {
  user({ ... }) {
    return getSomeData().then(res => return res)
  },
  profile({ ... }) {
    return getSomeData().then(res => return res)
  },
};

const routeMap = [{
  route: '/profile',
  fetchers: ['user', 'profile']
}, {
  route: '/home',
  fetchers: ['user']
}, {
  route: '/',
  fetchers: ['user']
}, {
  route: '/messages',
  fetchers: ['user']
}];

export default req => {
  let params, currentRouteConf;
  for (let i = 0; i < routeMap.length; i++) {
    const { route: path } = routeMap[i];
    const route = new Route(path); // 'npm install route-parser'
    const match = route.match(req.path);
    if (match) {
      params = match;
      currentRouteConf = routeMap[i];
      break;
    }
  }
  if (currentRouteConf) {
    const { fetchers: fetcherNames } = currentRouteConf;
    const fetcherPromises = fetcherNames.map(fetcher => typeof fetcher === 'string' ? fetchers[fetcher](req, params) : fetchers[fetcher.fetcher](req, params));
    return Promise.all(fetcherPromises)
      .then(responses =>
        fetcherNames.reduce((results, fetcher, index) => {
          fetcher = typeof fetcher === 'string' ? fetcher : fetcher.datakey;
          return {
            ...results,
            [fetcher]: responses[index],
          };
        }, {}))
      .catch(console.log);
  }
  return false;
};
  1. Renderer
....
export const renderView = (res, { name, data = {} }) => {
  res.setHeader('content-type', 'text/html; charset=utf-8');
  // this render is the markojs render function --> https://markojs.com/docs/rendering/#renderinput-callback
  require(`/path/to/login.marko.js`).render({
    ...res.locals,
    ...data,
  }, res);
};
....
  1. Finally your frontend will have another app.js which will take care of removing the loader from the DOM and will render the actual component in that route.
import polyfill from '@babel/polyfill';
import { h, render } from 'preact';
import Router from 'react-router';
import NotFound from './components/PageNotFound';
....
//imports
....


const onDocumentLoaded = async () => {
  const shellSpinnerElmt = document.querySelector('#shell-spinner');
  const contentElmt = document.querySelector('#content');

  store.dispatch(..actionsToBeDispatched)

  //Controlling the page spinner based on state changes
  store.subscribe(() => {
    const { spinner = {} } = store.getState();
    shellSpinnerElmt.classList[spinner.showSpinner ? 'remove' : 'add']('hide');
  });

  shellSpinnerElmt.classList.add('hide');
  // call the frontend React render function
  render(<Router>....</Router>)
};

onDocumentLoaded();

document.addEventListener('DOMContentLoaded', onDocumentLoaded);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment