Skip to content

Instantly share code, notes, and snippets.

@jeffhandley
Last active February 13, 2016 18:02
Show Gist options
  • Save jeffhandley/9dcfe349319fc3583161 to your computer and use it in GitHub Desktop.
Save jeffhandley/9dcfe349319fc3583161 to your computer and use it in GitHub Desktop.
A pattern for server-side async data loading with React components
import React from 'react';
export default (req, res, callback) => {
// Do async work, consuming data off req if needed
// Potentially set headers or other data on res
// When all the data is loaded, call the callback with the component
callback(React.createClass({
render() {
return (
<html>
<head />
<body />
</html>
);
}
}));
}
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import loadPage from './page';
const app = express();
app.get('/', (req, res) => {
loadPage(req, res, (Page) => {
res.send(ReactDOMServer.renderToStaticMarkup(<Page />));
});
});
app.listen(3000);
@jeffhandley
Copy link
Author

What I love about this pattern is that it is fractal. If a page needs to include a component that needs to preload its own data, the page can import the component's load function using the exact same contract, and when its callback is called, the page then produces itself using the loaded component.

Here's a page that loads a component:

import React from 'react';
import loadHeader from './components/header';

export default (req, res, callback) => {
  loadHeader(req, res, (Header) => {
    callback(React.createClass({
      render() {
        return (
          <html>
            <head />
            <body>
              <Header />
            </body>
          </html>
        );
      }
    });
  });
}

So the contract is simple:

  • Wrap data-loading component in a function that receives req and res and calls a callback with the component provided when data is loaded

These functions can be composed together anywhere in the hierarchy.

@jeffhandley
Copy link
Author

This pattern also composes nicely with react-router. While typical react-router routes would define the route component directly as a React component, we can instead define the component as the (req, res, callback) function seen in this pattern. The only "gotcha" here is that we then need to execute the load functions to retrieve the components before rendering them.

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import express from 'express';
import page from './page';
import { Router, Route, match } from 'react-router';

const app = express();

// Define our routes, setting the components to be the functions that load pages
// (rather than directly using the React components)
const routes = (
  <Router>
    <Route path='/' component={ page } />
  </Router>
);

app.get('*', (req, res) => {
  match( { location: req.url, routes }, (error, redirectLocation, renderProps) => {
    if (renderProps) {
      // For illustration, we'll just grab the last component in the hierarchy
      // Depending on your own patterns, you could extract the components
      // that need to be loaded in many different ways
      const loadPage = renderProps.components[renderProps.components.length - 1];

      // loadPage is now applying the same pattern as before we used react-router    
      loadPage(req, res, (Page) => {
        res.send(ReactDOMServer.renderToStaticMarkup(<Page />));
      });
    }
});

app.listen(3000);

@jeffhandley
Copy link
Author

Because pages can now be loaded asynchronously, and they can compose together async-loading components, a natural evolution is to introduce a page template concept over top of this pattern.

While react-router allows routes to have nested components with child routes, you are limited to only having direct children, where the children will be rendered as one block of content. This is limiting because it's very common for a "page" to need to contribute content into the header, side bar, main content area, and the footer--with each of these content blocks being isolated from one another. There may even be default content blocks for each template section, but the allowance for any page to override the sections.

Let's take a look at what a page template implementation looks like applying the async page loading pattern we're studying here. We'll start by looking at the resulting HTML we want to see for our page.

<html>
  <head>
    <title>{ page.title }</title>
  </head>
 <body>
    <div id="header">
      <!-- Header content provided by the default template, but possibly overridden by the page -->
    </div>
    <div id="sidebar">
      <!-- Header content provided by the default template, but possibly overridden by the page -->
    </div>
    <div id="page">
      <!-- Main page content -->
    </div>
    <div id="footer">
      <!-- Footer content provided by the default template, but possibly overridden by the page -->
    </div>
  </body>
</html>

Let's imagine we have a React component that represents this page template; here's what it might look like.

import React from 'react';

export default React.createClass({
  propTypes: {
    title: React.PropTypes.string.isRequired,
    header: React.PropTypes.element,
    sidebar: React.PropTypes.element,
    page: React.PropTypes.element.isRequired,
    footer: React.PropTypes.element
  },
  getDefaultProps() {
    return {
      header: <div>This is the default header</div>,
      sidebar: <div>This is the default sidebar</div>,
      footer: <div>This is the default footer</div>
  },
  render() {
    const { title, header, sidebar, page, footer } = this.props;

    return (
      <html>
        <head>
          <title>{ title }</title>
        </head>
       <body>
          <div id="header">{ header }</div>
          <div id="sidebar">{ sidebar }</div>
          <div id="page">{ page }</div>
          <div id="footer">{ footer }</div>
        </body>
      </html>
    );
  }
});

OK, this is a pretty straight-forward React component! Let's see what it would look like to define a page that uses this template and applies the data loading pattern we started out with.

import React from 'react';
import Template from './Template';

// Provide a title, header, and page - but use the default sidebar and footer
export default (req, res, callback) => {
  // Do async work before calling the callback
 callback(React.createClass({
  render() {
    return (
      <Template
        title='This is the page title'
        header={
          <div>This is the page's overridden header</div>
        }
        page={
          <div>This is the main page content</div>
        }
      />
    );
  }));
}

So far so good! But let's imagine the scenario where the page template itself has async work to do before it can be loaded. What would that look like? Well, we end up just nesting the async pattern (remember, it's fractal). Let's wrap the template in a (req, res, callback) function and import it into the page using the pattern.

import React from 'react';

export default (req, res, callback) => {
  // Do async work to load the template's data
  callback(React.createClass({
    propTypes: {
      title: React.PropTypes.string.isRequired,
      header: React.PropTypes.element,
      sidebar: React.PropTypes.element,
      page: React.PropTypes.element.isRequired,
      footer: React.PropTypes.element
    },
    getDefaultProps() {
      return {
        header: <div>This is the default header</div>,
        sidebar: <div>This is the default sidebar</div>,
        footer: <div>This is the default footer</div>
    },
    render() {
      const { title, header, sidebar, page, footer } = this.props;

      return (
        <html>
          <head>
            <title>{ title }</title>
          </head>
         <body>
            <div id="header">{ header }</div>
            <div id="sidebar">{ sidebar }</div>
            <div id="page">{ page }</div>
            <div id="footer">{ footer }</div>
          </body>
        </html>
      );
    }
  }));
}

We then update the page to load the template asynchronously.

import React from 'react';
import loadTemplate from './template';

// Provide a title, header, and page - but use the default sidebar and footer
export default (req, res, callback) => {
  // Load the Template component asynchronously
  loadTemplate(req, res, (Template) => {
    callback(React.createClass({
      render() {
        return (
          <Template
            title='This is the page title'
            header={
              <div>This is the page's overridden header</div>
            }
            page={
              <div>This is the main page content</div>
            }
          />
        );
      }
    }));
  }
}

Through all of this, the server.js that we used above will continue working as-is without any change. It loads the page asynchronously and when that is complete, it has a <Page /> to render. But now, the page can also load its page template asynchronously and render content into several template sections. The page template can provide default content for any section and it has the opportunity to fetch data as needed.

This fractal data loading pattern allows pages, page templates, and components to each load their own data asynchronously and provide React components when ready. The server code remains very simple along the way, keeping it decoupled from our flux loops, services, and related concerns.

@jeffhandley
Copy link
Author

We are contributing "containers" of content into page templates, where each container is rendered through React components that might need to asynchronously load data. But everything we've done so far is limited to server-side rendering with static markup--we're not doing any universal rendering yet--let's do that!

Let's look back at our page template to determine what we need to plug in.

import React from 'react';

export default (req, res, callback) => {
  // Do async work to load the template's data
  callback(React.createClass({
    propTypes: {
      title: React.PropTypes.string.isRequired,
      header: React.PropTypes.element,
      sidebar: React.PropTypes.element,
      page: React.PropTypes.element.isRequired,
      footer: React.PropTypes.element
    },
    getDefaultProps() {
      return {
        header: <div>This is the default header</div>,
        sidebar: <div>This is the default sidebar</div>,
        footer: <div>This is the default footer</div>
    },
    render() {
      const { title, header, sidebar, page, footer } = this.props;

      return (
        <html>
          <head>
            <title>{ title }</title>
          </head>
         <body>
            <div id="header">{ header }</div>
            <div id="sidebar">{ sidebar }</div>
            <div id="page">{ page }</div>
            <div id="footer">{ footer }</div>
          </body>
        </html>
      );
    }
  }));
}

Where we're rendering the header, sidebar, page, and footer, we need to introduce and affordance for any of those elements to be universally-rendered. Here's how we can tackle that problem:

  1. Each container needs to be able to be rendered on the client using ReactDOM.render(), targeting the <div> with the id
  2. That means we should also use ReactDOMServer.renderToString() to initially render those containers on the server
  3. But the outer template should still be rendered using ReactDOMServer.renderToStaticMarkup() so that it's plain HTML until we get down to a universally-rendering container (so that universal rendering is "clean")

The server.js code is presently rendering the entire page as static markup:

      loadPage(req, res, (Page) => {
        res.send(ReactDOMServer.renderToStaticMarkup(<Page />));
      });

That will stay as-is. But within the rendering of the <Page /> component, we need to render our template section containers using ReactDOM.renderToString(). We can tackle that within the render() function of our page template.

import React from 'react';
import _ from 'lodash';

export default (req, res, callback) => {
  // Do async work to load the template's data
  callback(React.createClass({
    propTypes: {
      title: React.PropTypes.string.isRequired,
      header: React.PropTypes.element,
      sidebar: React.PropTypes.element,
      page: React.PropTypes.element.isRequired,
      footer: React.PropTypes.element
    },
    getDefaultProps() {
      return {
        header: <div>This is the default header</div>,
        sidebar: <div>This is the default sidebar</div>,
        footer: <div>This is the default footer</div>
    },
    render() {
      // Grab the title and then gather the rest of the props into a containers object
      // the containers object becomes { header, sidebar, page, footer }
      const { title, ...containers } = this.props;

      // Render every container component into React markup,
      // getting the rendered results into the sections object
      const sections = _.mapValues(containers, (container) => ReactDOMServer.renderToString(container));

      // sections now has { header, sidebar, page, footer } -- each is a string of rendered HTML
      return (
        <html>
          <head>
            <title>{ title }</title>
          </head>
         <body>
            { /* We safely rendered each container; we can inject the HTML into the divs */ }
            <div id="header" dangerouslySetInnerHTML={{ __html: sections.header }} />
            <div id="sidebar" dangerouslySetInnerHTML={{ __html: sections.sidebar }} />
            <div id="page" dangerouslySetInnerHTML={{ __html: sections.page }} />
            <div id="footer" dangerouslySetInnerHTML={{ __html: sections.footer }} />
          </body>
        </html>
      );
    }
  });
}

With this, we are:

  1. Rendering the outer page as static markup
  2. When that happens, we're rendering the containers to React markup and putting that markup into container divs

This has a result of "islands" of React markup inside a sea of static markup, without the server code having any idea it's happening. We're now positioned to introduce client-side rendering into those containers.

@jeffhandley
Copy link
Author

With the server rendering isolated containers of React content that can be universally rendered, it's time to determine what else we will need to accomplish that universal rendering.

Each universally-rendered container is likely to require:

  1. Initial state, dehydrated from the server (from whatever flux implementation was involved)
  2. A client entry point that loads that initial state, starts the flux loop, and renders components into the container

This is where the react-composite-pages project (npm package) comes in. React-Composite-Pages introduces a <RenderContainer /> component that allows React components to be encapsulated in universal rendering containers, with their state and client scripts tagging along (but rendered into the page structure where the page template dictates). React-Composite-Pages is ignorant of any flux implementation choice--it can be used with any flux implementation or even without one.

Modifying the page and the page template from above to use React-Composite-Pages has the following result.

page.js

import React from 'react';
import { RenderContainer } from 'react-composite-pages';
import loadTemplate from './template';

// Provide a title, header, and page - but use the default sidebar and footer
export default (req, res, callback) => {
  // Load the Template component asynchronously
  loadTemplate(req, res, (Template) => {
    const headerState = { text: 'This is the page's overridden header' };
    const pageState = { text: 'This is the main page context' };

    callback(React.createClass({
      render() {
        return (
          <Template
            title='This is the page title'
            header={
              <RenderContainer
                id='page-header'
                state={ headerState }
                clientSrc='/client/header.js'>
                  { headerState.text }
              </RenderContainer>
            }
            page={
              <RenderContainer
                id='page-body'
                state={ pageState }
                clientSrc='/client/page.js'>
                  { pageState.text }
              </RenderContainer>
            }
          />
        );
      }
    }));
  }
}

template.js

import React from 'react';
import { RenderContainer } from 'react-composite-pages';
import _ from 'lodash';

export default (req, res, callback) => {
  // Do async work to load the template's data
  callback(React.createClass({
    propTypes: {
      title: React.PropTypes.string.isRequired,
      header: React.PropTypes.element,
      sidebar: React.PropTypes.element,
      page: React.PropTypes.element.isRequired,
      footer: React.PropTypes.element
    },
    getDefaultProps() {
      return {
        header: <div>This is the default header</div>,
        sidebar: <div>This is the default sidebar</div>,
        footer: <div>This is the default footer</div>
    },
    render() {
      // Grab the title and then gather the rest of the props into a containers object
      // the containers object becomes { header, sidebar, page, footer }
      const { title, ...containers } = this.props;

      // Render every container component into React markup,
      // getting the rendered results into the sections object
      const template = RenderContainer.renderTemplate(containers);

      // template now has: { state, clients, sections: { header, sidebar, page, footer } }
      // Each of those properties is a React component
      return (
        <html>
          <head>
            <title>{ title }</title>
          </head>
          <body>
            <template.sections.header />
            <template.sections.sidebar />
            <template.sections.body />
            <template.sections.footer />
            <template.state />
            <template.clients />
          </body>
        </html>
      );
    }
  });
}

If we examine the HTML that comes out of this we'll see:

<html>
<head>
  <title>This is the page title</title>
</head>
<body>
  <div>
    <div>
      <div id="page-header"><span data-reactid=".1dn7u7gyvi8.0">This is the page's overridden header</span>
      </div>
      <noscript></noscript>
      <noscript></noscript>
    </div>
    <div>This is the default sidebar</div>
    <div>
      <div id="page-body"><span data-reactid=".1sdsfsy3nsyf.0">This is the main page context</span>
      </div>
      <noscript></noscript>
      <noscript></noscript>
    </div>
    <div>This is the default footer</div>
    <script>
      window.RenderState = {
        "page-header": {
          "text": "This is the page's overridden header"
        },
        "page-body": {
          "text": "This is the main page context"
        }
      };
    </script>
    <div>
      <script src="/client/header.js"></script> 
      <script src="/client/page.js"></script> 
    </div>
  </body>
</html>

If you're wondering, the <noscript> tags are the result of using react-side-effect within RenderContainer to "export" the state and clients. Each RenderContainer actually emitted a <RenderState> and <RenderClient> component (from react-composite-pages) into the component hierarchy, but they don't result in any rendered content--instead, they contribute to the template.state and template.clients components that came out of the renderTemplate() function.

As you can see here, react-composite-pages provides a useful RenderContainer component that can be used to apply this pattern of universal rendering containers that each have their own state and clients.

From here, the next step is to jump into the examples provided in the GitHub project for React-Composite-Pages and see how this comes together with flux implementations (there are redux and fluxible pages), webpack, and completing the client-side rendering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment