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

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