I’ve been dabbling with React for a few months now and using it in several small open source projects to better understand the technology. React’s focus on reusablility, along with the ability to install and require components via npm, provides an elegant way to rapidly build application UI in an efficient and consistent way. It’s also a great way to handle server-side rendering and provides high cohesion between markup and display logic.
React is incredibly intuitive and flexible to work with, but relying solely on client side rendering leaves a lot to be desired in regards to percieved performance. With a static React site, the page loads just like any other static HTML, and the JavaScript kicks in whenever it’s downloaded. This is extremely helpful for combining documentation with interactive demonstrations, like Colorable or Building SVG Icons with React. Using React with webpack also allows you to consolidate the entire build process in Node and take advantage of anything in the npm ecosystem. Last but not least, React is just fun to use.
Although there are several ways to render static sites with React, the following is what I’ve found to be easiest.
First start a fresh project and initialize npm.
mkdir react-static-site
cd react-static-site
npm init
Install the following modules.
npm i --save-dev react webpack webpack-dev-server jsx-loader static-site-generator-webpack-plugin react-router
Webpack and webpack-dev-server will be used for the entire build process and can also handle assets such as CSS, images, and fonts. The jsx-loader is used to transpile imported jsx files. If you prefer ES6, you can use babel-loader instead of jsx-loader. The static-site-generator-webpack-plugin module is what is used to generate static HTML. And React Router is used to handle routing. For single page static sites, React Router is not needed.
Webpack is a module bundler similar to Browserify, but can also replace front-end build systems like Grunt and Gulp.
First off, you’ll need a webpack.config.js
file. The webpack command line interface will use this config file.
var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin')
var data = require('./data')
module.exports = {
entry: './entry.js',
output: {
filename: 'bundle.js',
path: __dirname,
libraryTarget: 'umd'
},
module: {
loaders: [
{ test: /\.jsx$/, loader: 'jsx-loader' }
]
},
plugins: [
new StaticSiteGeneratorPlugin('bundle.js', data.routes, data)
]
}
The entry file is what webpack will read to build bundle.js
,
and the static-site-generator-webpack-plugin uses the bundle to generate HTML.
For a single rendered page, you can skip React Router and create an entry.js
file like the following.
var React = require('react')
var Root = require('./components/Root.jsx')
module.exports = function render(locals, callback) {
var html = React.renderToStaticMarkup(React.createElement(Root, locals))
callback(null, '<!DOCTYPE html>' + html)
}
For handling multiple routes, update the entry file using React Router and create a Routes.jsx
file.
var React = require('react')
var Router = require('react-router')
var Routes = require('./Routes.jsx')
module.exports = function render(locals, callback) {
Router.run(Routes, locals.path, function (Handler) {
var html = React.renderToStaticMarkup(React.createElement(Handler, locals))
callback(null, '<!DOCTYPE html>' + html)
})
}
var React = require('react')
var Router = require('react-router')
var Route = Router.Route
var DefaultRoute = Router.DefaultRoute
var Root = require('./components/Root.jsx')
var Index = require('./components/Index.jsx')
var Routes = (
<Route handler={Root} path='/'>
<DefaultRoute handler={Index} />
</Route>
)
module.exports = Routes
Next create a data.js
file that will include initial props passed to the Root component and routes used for the router.
module.exports = {
title: 'My Static Site',
routes: [
'/'
]
}
The Root component will include the <html>
element, <head>
and other code that will be shared across all pages.
The page components themselves will be passed through the <RouteHander>
component with React Router.
To keep things somewhat organized, create this file in a new components
directory.
var React = require('react')
var Router = require('react-router')
var RouteHandler = Router.RouteHandler
var Root = React.createClass({
render: function () {
return (
<html>
<head>
<title>{this.props.title}</title>
</head>
<body>
<RouteHandler {...this.props} />
</body>
</html>
)
}
})
module.exports = Root
The Index component will be the page rendered with <DefaultRoute>
and should contain the content for the root index.html
file.
var React = require('react')
var Index = React.createClass({
render: function () {
return (
<main>
Index component
</main>
)
}
})
module.exports = Index
Add the following scripts to package.json
to run webpack.
"scripts": { "webpack": "webpack --progress --colors", "dev": "webpack-dev-server --progress --colors", "start": "npm run dev" }
Run the start script to start a development server.
npm start
Open http://localhost:8080 in a browser. You should see an unstyled page with the words Index component.
Next, add a new route for an about page.
In data.js
add /about
to the routes array.
module.exports = {
title: 'My Static Site',
routes: [
'/',
'/about'
]
}
Edit the Routes.jsx
file to handle the new route.
var React = require('react')
var Router = require('react-router')
var Route = Router.Route
var DefaultRoute = Router.DefaultRoute
var Root = require('./components/Root.jsx')
var Index = require('./components/Index.jsx')
var About = require('./components/About.jsx')
var Routes = (
<Route handler={Root} path='/'>
<DefaultRoute handler={Index} />
<Route path='/about' handler={About} />
</Route>
)
module.exports = Routes
Create a new About.jsx
component.
var React = require('react')
var About = React.createClass({
render: function () {
return (
<main>
About component
</main>
)
}
})
module.exports = About
Stop and restart the development server and navigate to http://localhost:8080/about. You should see a page similar to the index but with the words About component.
React-router can also handle route params such as posts/:id
for dynamic routing –
View the docs to learn more.
Stop the development server and run npm run webpack
. This should generate two static index.html
files in the root and about
directories.
In order to use React for client side JavaScript, you’ll need to ensure that the props match up between the static page and the bundle.js file.
First, edit entry.js
to change the render function
from React.renderToStaticMarkup
to React.renderString
and add a conditional block that will only run client-side.
var React = require('react')
var Router = require('react-router')
var Routes = require('./Routes.jsx')
if (typeof document !== 'undefined') {
var initialProps = JSON.parse(document.getElementById('initial-props').innerHTML)
Router.run(Routes, Router.HistoryLocation, function (Handler) {
React.render(React.createElement(Handler, initialProps), document)
})
}
module.exports = function render(locals, callback) {
Router.run(Routes, locals.path, function (Handler) {
var html = React.renderToString(React.createElement(Handler, locals))
callback(null, '<!DOCTYPE html>' + html)
})
}
The initialProps
value will come from a script tag with the id initial-props
.
Update the Root component to add this script tag, add a safeStringify
function, and link to the bundle.js
file.
var React = require('react')
var Router = require('react-router')
var RouteHandler = Router.RouteHandler
var Root = React.createClass({
render: function () {
var initialProps = {
__html: safeStringify(this.props)
}
return (
<html>
<head>
<title>{this.props.title}</title>
</head>
<body>
<RouteHandler {...this.props} />
<script
id='initial-props'
type='application/json'
dangerouslySetInnerHTML={initialProps} />
<script src='bundle.js' />
</body>
</html>
)
}
})
function safeStringify(obj) {
return JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--')
}
module.exports = Root
To link the pages together, create a new Header component.
var React = require('react')
var Header = React.createClass({
render: function () {
return (
<header>
<a href='/'>Index</a>
<a href='/about'>About</a>
</header>
)
}
})
module.exports = Header
Add the Header to the Root component’s render function.
var React = require('react')
var Router = require('react-router')
var RouteHandler = Router.RouteHandler
var Header = require('./Header.jsx')
var Root = React.createClass({
render: function () {
var initialProps = {
__html: safeStringify(this.props)
}
return (
<html>
<head>
<title>{this.props.title}</title>
</head>
<body>
<Header />
<RouteHandler {...this.props} />
<script
id='initial-props'
type='application/json'
dangerouslySetInnerHTML={initialProps} />
<script src='bundle.js' />
</body>
</html>
)
}
})
function safeStringify(obj) {
return JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--')
}
module.exports = Root
React router can also do client-side routing using the Link component. This can make transitioning pages feel faster and behaves like a fake single-page application.
To use client-side routing, replace the anchor links in the Header with React Routers’s Link components.
var React = require('react')
var Router = require('react-router')
var Link = Router.Link
var Header = React.createClass({
render: function () {
return (
<header>
<Link to='/'>Index</Link>
<Link to='/about'>About</Link>
</header>
)
}
})
module.exports = Header
Although there are many different approaches to styling components in React, adding some critical CSS base styles to the head can help speed up performance and development time. You can skip this step if you prefer using inline styles or linking to a larger stylesheet.
UPDATE: Originally, this tutorial showed how to include CSS using props.
Using a combination of css-loader and cssnext-loader seems to be a better solution
as it doesn't require restarting the server when making changes to the stylesheet
and doesn't duplicate the CSS in the initialProps
JSON object.
First install Basscss, css-loader, and cssnext-loader.
npm i --save-dev basscss css-loader cssnext-loader
Add css-loader and cssnext-loader to the webpack config as well as options for cssnext.
var StaticSiteGeneratorPlugin = require('static-site-generator-webpack-plugin')
var data = require('./data')
module.exports = {
entry: './entry.js',
output: {
filename: 'bundle.js',
path: __dirname,
libraryTarget: 'umd'
},
module: {
loaders: [
{ test: /\.jsx$/, loader: 'jsx-loader' },
{ test: /\.css/, loader: 'css-loader!cssnext-loader' }
]
},
plugins: [
new StaticSiteGeneratorPlugin('bundle.js', data.routes, data)
],
cssnext: {
compress: true,
features: {
rem: false,
pseudoElements: false,
colorRgba: false
}
}
}
In the cssnext configuration options, the output is compressed and the rem and colorRgba postcss plugins have been disabled.
Create a new css
directory and a base.css
file.
Import Basscss and set a new value for the --font-family
custom property.
@import 'basscss';
:root {
--font-family: 'Avenier Next', 'Hevletica Neue', sans-serif;
}
In the Root component, import the stylesheet, add a style tag to the head, and add some padding to the body.
var React = require('react')
var Router = require('react-router')
var RouteHandler = Router.RouteHandler
var Header = require('./Header.jsx')
var css = require('../css/base.css')
var Root = React.createClass({
render: function () {
var initialProps = {
__html: safeStringify(this.props)
}
return (
<html>
<head>
<title>{this.props.title}</title>
<style dangerouslySetInnerHTML={{ __html: css }} />
</head>
<body className='p2'>
<Header />
<RouteHandler {...this.props} />
<script
id='initial-props'
type='application/json'
dangerouslySetInnerHTML={initialProps} />
<script src='bundle.js' />
</body>
</html>
)
}
})
function safeStringify(obj) {
return JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--')
}
module.exports = Root
Restart the dev server to see the changes.
You should now have a basic static site rendered with React. For a complete example, see the Boilerplate Demo or check out the source for this blog, which uses a very similar approach.
Since this uses webpack, there are also ways to include image assets and fonts in the bundle, but I haven’t tried this so your mileage may vary.
The routes
array is passed in as props, and navigation links could be created dynamically rather than being hard coded.
This could make handling lots of pages easier.
If you’re hosting the static site on gh-pages, you’ll need a way to handle the base url when using React Router’s Link component. I don’t know of a good way to do this yet and would love to hear suggestions on how to improve that.
Handling the CSS as shown above can lead to a fairly large chunk of JSON being inserted into the initial-props script tag,
and I’m not sure if there’s a better way to handle that.
This is just one way to build static sites with React. If you’ve seen other ways or have any suggestions for improving on this, I’d love to hear them.