-
-
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); |
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.
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.
Where we're rendering the
header
,sidebar
,page
, andfooter
, we need to introduce and affordance for any of those elements to be universally-rendered. Here's how we can tackle that problem:ReactDOM.render()
, targeting the<div>
with the idReactDOMServer.renderToString()
to initially render those containers on the serverReactDOMServer.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:That will stay as-is. But within the rendering of the
<Page />
component, we need to render our template section containers usingReactDOM.renderToString()
. We can tackle that within therender()
function of our page template.With this, we are:
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.