14th April 2019

How, and why, we built our new WordPress site in React, with SSR in Timber/Twig

18 min. read

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.

Why React?

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:

  1. CSS-in-JS is becoming one of the most popular ways to build front end. It’s obscure to approach website building this way unless you’re using a UI library like React.
  2. React is becoming ubiquitous and many influential developers in the community are getting involved. We knew we had to keep up with the technology evolution of the web, even if React may seem like an odd choice for “traditional” website development.
  3. React is fast and let’s us render critical css directly into the page. This is good for performance because you no longer require an external stylesheet.
  4. React is really fun!

What becomes possible when using React

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?

We built a (relatively) simple way to create a server bundle in CRA that we use to render markup with twig (supported by the amazing Timber plugin).

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.

What is Twig / Timber?

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.

Precompiling React / Twig templates for SSR

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.

Next we needed to build proper support for this method of rendering with Webpack.

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 generates our static twig files

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).

Quick recap of how this all works together:

  1. We have a redux state for twig and one for real values from WordPress (JSONified).
  2. Inside the app are HoCs which render either twig state or real state depending on what context we’re passing to our app .
  3. Using an extended CRA webpack configuration, we provide an extra server bundle with special extended config for rendering from a separate preload.js file.
  4. By watching assets in our webpack configuration we’re able to provide a json file of assets at develop / production build time that WordPress can easily enqueue.
  5. In our preload.js file we can manipulate the static HTML as we see fit, such as doing some cleanup, and injecting Emotion styles in a separate style tag.

Okay, but this sounds all sounds a bit over-engineered.

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"
})

What about performance, in terms of bundle size. Are you loading one giant bundle in for all the pages in your site?

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.

What else can go wrong?

  1. Kinsta and WPE aggressively static cache sites. For this reason, when you’re loading a route as ajax, you must pass a parameter that distinguishes your ajax routes from normal page loads. Otherwise you’ll end up either with a broken React app, or a page full of JSON.
  2. React router doesn’t like poorly formed 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.
  3. Surprisingly, if you take enough care, hydration works perfectly with the twig preloaded files, but you should be aware of content coming from the CMS that won’t be available to the app. For example, if you’re storing the name of an icon as a string, React isn’t going to be bundling the icon at compile time, so hydration is not going to work perfectly in this case. But that’s okay, you can always suppress hydration warnings in specific places, or load icons only on mount. Hydration warnings also don’t appear in production builds.
  4. Emotion (and this most likely applies to styled-components et al) can break if you don’t extract your css from the app components. Twig conditionals can completely remove some of the styles we need, which are only embedded once per component. The result is half-styled components because some of the styles have been omitted by Timber/Twig! The solution here is to extractCritical the styles away from your app, and insert these styles before any twig conditionals may be occurring (probably outside <div id="root />.
  5. styled-system likes to add all kinds of attributes into your markup, which leads to poor validation. We got around this by creating a customised version of styled and using shouldForwardProp to weed out the bad attributes.

What went right, and is this framework feasible?

  1. Preloading routes is trivial, so by the time the user starts clicking, they’re likely to instantly access the page.
  2. We’re able to add an awesome Spring transition router (performance TBD)
  3. We’re able to use Css-in-js and styled-system. (Give yourself some some time to get used to these tools).
  4. CRA dev mode flags markup issues (including security issues, validation and accessibility errors, and the like).
  5. We can have a look at hydration issues to determine what may not be appropriately rendered in the client.
  6. We can render critical css inline for improved performance
  7. Adding new features is really fun. Even something as dreary as the cookie warning was fun to work with.
  8. We can tap the community for awesome react components, such as a dedicated Recaptcha component.
  9. We can still use jQuery with ease. We’re using flickity for sliders that we simply wrap in a useEffect hook in order to bind it to our components.

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!

How, and why, we built our new WordPress site in React, with SSR in Timber/Twig https://lab19.dev/how-and-why-we-built-our-new-wordpress-site-in-react-with-ssr-in-timber-twig/...
Share tweet

Are you looking for an experienced team of creatives and engineers to help with a ReactJS or WordPress project?

Chat to us now
Want to build something great? We’d love to help.

Trading hours:
Mon – Fri: 9am – 5:30pm (BST)
Closed on bank holidays

©
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

We use cookies on this site for basic tracking, and to enhance your experience. Find out more

Dismiss