-
-
Save jeffhandley/9dcfe349319fc3583161 to your computer and use it in GitHub Desktop.
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); |
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:
- Each container needs to be able to be rendered on the client using
ReactDOM.render()
, targeting the<div>
with the id - That means we should also use
ReactDOMServer.renderToString()
to initially render those containers on the server - 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:
- Rendering the outer page as static markup
- 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.
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:
- Initial state, dehydrated from the server (from whatever flux implementation was involved)
- 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.
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 directchildren
, 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.
Let's imagine we have a React component that represents this page template; here's what it might look like.
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.
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.We then update the page to load the template asynchronously.
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.