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

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