Well that’s a mouthful. This is a deep dive into how we dog-fooded a proof of concept in using both React / Timber on our site, without requiring a NodeJS backend to render SEO-friendly urls. First a little bit of info about React, and why we like it.
React has developed a huge following, and continues to do so with the introduction of hooks, which is all the rage at the moment in the developer Twittersphere. React is exceptionally well documented, and has a very mature community of developers who maintain various tools around it, such as Create React App, Redux, React Router, among many others.
While React isn’t really a framework, rather just a declarative and intuitive way to organise UI, it’s fast morphing into one when you consider what features from the community are at your disposal, and how mature these open source projects now are.
With this in mind, we knew we had to build our new site on React, for a few reasons:
There are a host of other reasons you might want to use React in your team. For example, we can reuse components developed in our new website for other clients. We can maintain a repository of components that deal with certain challenges, such as Ajax-enabled pagination, or Spring animations between routes. It’s not impossible to do this with other approaches, but React lets you place much of your logic and styles into a single file, which makes reusability and team visibility much easier.
The best thing about React is it allows fairly trivial implementation of what would normally be exceptionally complex approaches to problems
React in a nutshell
We’re also able to use some amazing libraries from the community, including Spring for transitions, Redux for easy (and well documented) state management, React router which solves most routing challenges, and Loadable, for code splitting / component loading on demand for improved load times.
The best thing about React and its functional programming roots is it allows fairly trivial implementation of what would normally be exceptionally complex approaches to problems like lazy loading chunks of components into a page at the correct moment, preloading bits of essential content, deferring load of expensive items, and other asynchronous pain. With traditional approaches, you’ll end up with large files of nested loops, or endlessly chained promises, but with React’s unidirectional data flow and relatively simple lifecycle methods your code can still be succinct and declarative.
Anyway, that’s enough about how great React is. Since we made the choice, what were some of the challenges around building a WordPress site with it, and how the heck could we server-side render React without a NodeJS server in order to preserve SEO value?
That’s also a mouthful. So what exactly does this approach entail?
We knew we wanted to use CRA (Create React App), because it’s the most adopted starter framework for React out there, and supports most of what React developers need. However, by default it does not allow server-side rendering – it only offers a way to build a production client-side bundle. For server-side rendering you are generally expected to solve this another way, probably by using a completely separate webpack config with a “similar” setup to the one provided by CRA. This poses some maintenance problems, and since we were actually going to be generating server-side files at dev time, we thought it may make more sense just to extend react-scripts ever so slightly to provide the functionality we need.
This is where a light sprinkling of Webpack plugins came to the rescue, and the little known feature of CRA which allows you to create react app with a different “react-scripts” package.
But before we dive too deep into these specifics, let’s explain conceptually how Timber/Twig rendering could even be achieved with React and why you would want to do it that way.
Timber is a great WordPress plugin from Boston/NYC agency Upstatement, that lets you render your theme in twig files. Twig is a clean templating language that avoids all that PHP code mixing in with your markup. Your logic is maintained largely with PHP files and Timber defaults, and Timber extends twig to allow for common WordPress functionality to be accessible from within twig. This amounts to a very user-friendly API, and makes WordPress theming fun again!
<!-- Look how shiny! -->
<h1>{{ post.title }}</h1>
<div>
{{ post.content }}
</div>
Because of twig’s declarative styles, it seemed like a natural fit with what we wanted to achieve, which is “prebuilt” templates for SSR. So let’s deep dive into what this really means.
When React normally server-renders, it needs all the information in order to do so. But is it really necessary to provide React with all the information at runtime? Can’t we just dump down our React markup with dynamic values inside it, and render those in a traditional PHP / Twig environment? Yes, it turns out we can (with reasonable elegance), because really any kind of output is possible within React.
Consider the following pseudo-code example. Let’s assume we have WordPress state available at this time:
function BlogPost( { wordpress } ){
return (
<div>
<h1>{ wordpress.title }</h1>
<div>{ wordpress.content }</div>
</div>
)
}
If we wanted to server-side render this component a traditional way, we would need to supply the WordPress state from an API such as GraphQL wrapping the WordPress API. That’s okay, but we need a NodeJS server to do this, and a relatively specific configuration on that server. We can’t just use Kinsta or WP Engine, or some other WordPress host out the box like most developers might want to.
But there’s a solution here which doesn’t involve backend Node at all. We provide data in 2 states, the normal client state we’re all familiar with, and a server-side state which is declarative twig. So another pseudo example looks like this:
function Post( props ){
// Server-side render
if(props.server){
return <BlogPost wordpress={{
title: "post.title",
content: "post.content"
}} />
} else {
return <BlogPost wordpress={{
title: "My first post",
content: "..."
}} />
}
}
This may look a bit cumbersome, but in reality this can be abstracted away using an HOC (Higher Order Component), like so:
export const BlogPost = withWordpress( {wordpress} => (
<div>
<h1>{ wordpress.title }</h1>
<div>{ wordpress.content }</div>
</div>
))
We don’t need to keep track any more of what state we’re in. We just know at this point, that the WordPress prop will contain what we need, and inside the HoC we can do all the grunt work like figuring out whether we’re in server-side rendering mode.
Our prebuilt template roughly ends up looking something like this:
{# build/twig/post.twig #}
<div><h1>{{ data.title }}</h1><div>{{ data.content }}</div></div>
{# Embedded JSON for hydration in React #}
<script type="text/json" id="initialState">{{ data|json_encode }}</script>
SEO-ready once it’s rendered by Timber, and also ready to be hydrated!
This is a simple example, but things do get more complicated when you introduce loops, nested data, and conditional statements (which are still required in order to properly respect SEO and not output big empty components). We’ll get to that a bit later.
As mentioned before CRA does not support SSR out the box, and even if it did, it would probably not suit our implementation (which is a little unusual even for the React world).
We needed a way to generate our markup files containing twig, and save them to the build folder where the CRA client build lives. Then in PHP / Timber we would be able to load these files in for the respective routes, thereby presenting an SEO-rendered version of the page, exciting!
We’re able to do this by forking CRA (not in a very aggressive way), but there’s actually a documented way to do this, a discussion of which can be found here: https://github.com/facebook/create-react-app/issues/682. Essentially this allows you to create a “customised” react-scripts app using the command create-react-app my-app --scripts-version my-react-scripts-fork
.
This allows us to extend CRA Webpack (and we try do so carefully so that’s easy to merge upstream changes from CRA). Here’s a pseudo example of how this is done:
# webpack.config.override.js
const SaveAssetsToDisk = require('./webpack-plugin-save-assets-to-disk')
const PreloadServer = require('./webpack-plugin-preload-server')
module.exports = function(webpackEnv){
const config = configFactory(webpackEnv)
// Do some stuff to config... then return as normal
config.plugins = config.plugins.concat([
new SaveAssetsToDisk({
pathRoot: paths.appBuild,
name: 'assets.json'
}),
new PreloadServer({
webpackEnv: webpackEnv
})
])
return config
}
We extend the configuration, create what we’ve named a “preload server” based on the same configuration as CRA, but make a couple of tweaks (into the client config we push a plugin to SaveAssetsToDisk, and we push our PreloadServer).
Behind the scenes in SaveAssetsToDisk, we hook into HtmlWebpackPlugin hooks and tap the hook onBeforeHtmlGeneration in order to grab a list of assets. We then write these down to a json file in the build folder. This let’s us access the paths to the assets in WordPress so when we refresh our theme we can see everything loading correctly!
PreloadServer alters the webpack to make it suitable for SSR, like limiting the chunks into a single entry file called preload.js, removing minification and splitChunks / runtimeChunk, changing the target to Node, and then choosing MemoryFS as the outputFileSystem. There’s basically just several changes required to get CRA webpack to behave comfortably as a SSR solution. Bear in mind that we hardly change the client side webpack bundle, and remaining pretty close to upstream will let us use the goodness of CRA in future without overcomplicating our lives.
const preloadServer = webpack(preloadConfig)
preloadServer.outputFileSystem = new MemoryFS()
preloadServer.run(function (err, stats) {
const src = stats.compilation.assets['preload.js'].source()
// At this point we do stuff with src
// like require it from string and execute methods
})
The preload bundle having been generated in the memory filesystem expects a single method to be returned. We execute this method for every file change or when building an optimised production build. In this file we can completely customise how our app renders. We import our app as normal, we pass a Redux store of twig state (instead of real state), and now we tell our HoC that we’re rendering SSR, and we use the StaticRouter provider by React Router to render all the routes we have defined in our app.
We also need to do a few other things in preload.js but this really depends on your build. In our build we use Emotion for css-in-js rendering, and in our preload file we create a custom emotion server in order to extract the css (extractCritical) as inline styles, pulling them out of the app components.
function render(store) {
const context = {}
let cache = createCache()
let { extractCritical } = createEmotionServer(cache)
let { html, css, ids } = extractCritical(
renderToString(
<CacheProvider value={cache}>
<Provider store={store}>
<StaticRouter context={context}>
<App server={true} />
</StaticRouter>
</Provider>
</CacheProvider>
)
)
return `<style data-emotion-css="${ids.join(" ")}">${css}</style><div id="root">${html}</div>`
}
We also need to do a bit of cleanup of our static render inside preload.js, getting rid of a few tiny quirks with how React-router treats prop.to when passing a twig string like {{ post.pathname }}
(tldr; it adds a leading slash which breaks things).
We won’t entirely argue with that point, but this remains a solution to a problem of “what does my host support?”, and “Can I really use React in a WordPress theme, what about SEO, how can I render a React app without NodeJS support?”. It’s also been really fun to dog-food the concept in our site and learn about some of the pitfalls of the approach. Let’s get into that now.
Common pitfalls
1. Conditionals. These don’t work as you may expect if you aren’t familiar with the project. We need a helper to render a conditional, like this:
<Twig ifDefined={ wordpress.some_value }>
<Component />
</Twig>
Which effectively behaves differently in its two forms, in server-side it outputs Twig markup around the component (as strings in html):
{% if wordpress.some_vaue %}
<Component />
{% endif %}
But on the client it doesn’t need to do that, it can just use a normal JS conditional because by this time, the value is available (e.g. via Ajax API or json inside HTML):
{ wordpress.some_value ? <Component /> : null }
This can easily lead to confusion for developers. They’ll use conditionals as normal, but twig state is always present on the server-side, so the component will always be displayed unless you appropriately wrap it in a helper function that renders twig markup. As a developer, you need to be conscious all the time of being in 2 states, the twig state and the client state. This is difficult because there’s no obvious declarative solution here. The only solution appears to be documenting things very well, and creating what is essentially a WordPress / Timber / Twig / React theming framework, yikes!
2.Twig state gets deep, very deep
Another problem we encountered was the depth of twig state when using something like ACF layouts, repeaters, groups, etc. We solved this through recursing through data and generating a twig state object which holds various information about twig.
We won’t go too much into this here, suffice to say it’s just a logic challenge, and there needs to be a bunch of tests written around this to ensure that no one gets tripped up by it in future. The structure is needed to not only store twig data but also twig declarations. We need to know both the name of an item, and whether or not it is an array we can iterate through. This translates roughly to objects that can store meta information, something like:
{ __key: "wordpress.posts", __value: [] }
3. We need to carefully prepare data in both twig and React.
Twig needs to pass structured JSON data to the client. It also needs to hydrate that data (we do this embedded, bootstrapped JSON), and it needs to retrieve data from routes via pseudo-API calls (really just WordPress query data output as JSON).
Then the twig state needs to be registered, so that redux knows about all these values.
We experimented with doing this in various files and HoCs, but the conclusion was that things became too complex over time to track, so we settled on having just two files.
_data.twig
Organises and arranges the entirety of the site’s twig state
{% set data = {
title: post.title,
content: post.content
} %}
registerTwigState.js
Has a very similar structure to the above, and provides the initialState for redux, so that the app can safely render values for server-side rendering. This file basically declares what variables might be available from twig, but their actually value doesn’t matter, just that they might exist.
registerTwigState({
title: "data.title",
content: "data.content"
})
This isn’t the case because we use Loadable, which let’s us easily code split our routes. They load on demand, and we also preload them at a suitable moment so that the site feels even faster once it’s loaded. It’s another great reason for using React, because it lets you trivially implement optimisations like this. You can code split with relative ease, but you do need to be aware that all your chunks must be available and ready to go in your server-side bundle.
For this reason we preloadAll
loadable files initially before we start rendering possible route templates.
to
props, such as <Link to="{{ post.path }} />
. It adds a trailing slash. Before saving it to disk, we perform some basic string replacement on static markup to fix the issue. Then the app hydrates the component correctly too. extractCritical
the styles away from your app, and insert these styles before any twig conditionals may be occurring (probably outside <div id="root />
. styled
and using shouldForwardProp
to weed out the bad attributes. We like to think that the dog-fooding approach is going to prove the worthiness of this framework for the mainstream. Our next steps will be to extract the Webpack plugin code and other helpers into libraries with mature APIs, and to document this approach in a way that minimises confusion.
Whether or not the framework proves to be valuable to the WordPress community, as they continue to strive towards API backends, will remain to be seen. For now we’re able to solve server-side rendering without NodeJS on the backend, and we’re able to freely build components for our site in React. Which feels great!
Are you looking for an experienced team of creatives and engineers to help with a ReactJS or WordPress project?
Trading hours:
Mon – Fri: 9am – 5:30pm (BST)
Closed on bank holidays
We use cookies on this site for basic tracking, and to enhance your experience. Find out more