Forest header image

Symfony Finland
Random things on PHP, Symfony and web development

Attaching React.js to a template rendered on the server with Twig

React.js is a JavaScript view library that allows developers to create interfaces is a structured way based on a hierarchical component structure. React can either create the DOM structure from scratch, or attach to an existing one rendered by the server to speed up first load.

If you create Twig templates that match the React rendering, you can take advantage of this feature without a complicated rendering setup.

By default developers working with React will be working with client side rendering, and the server side rendering of React has been somewhat of a confusing topic in the past. But with React 16.0, there were improvements that make it more clear and feasible to implement.

Prior to React 16, the library relied on attributes in the HTML to identify and be able to attach to the DOM. This resulted HTML markup littered with "data-react" attributes:

<div class="item-content-left" data-reactid="862"><div
class="item-time-container" data-reactid="863"><time
class="item-time" title="su 12.11. 12:30" datetime=
"2017-11-12T10:30:12.000Z" data-reactid="864">

Generating these attributes dynamically with anything except Node.js was not practical, and developers have resorted into alternative ways of rendering React.js with PHP on server side.

Using React 16 with Twig templates

There is no longer anything special in the rendered HTML required for React.js to be able to attach to. This makes creating compatible markup easier to do. It is now feasible to generated server side rendered (SSR) markup with any technology, as long as the structure matches what React expects.

Now let's consider a simple React application:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

class App extends React.Component {
  render() {
  return <table>
    <tbody>
    <tr>
      <th>Name</th>
      <th>Address</th>
      <th>Age</th>
    </tr>
    { initialState.employees.map( (employee) => {
      return <tr key={employee.uuid}>
      <td>{employee.name}</td>
      <td>{employee.address}</td>
      <td>{employee.age}</td>
      </tr>
    }) }
    </tbody>
  </table>
  }
}

ReactDOM.render(<App />, document.getElementById('employee-list'));

This will yield a simple table with some information on employees, which will be a standard DOM structure in the browser. This is initiated by a call to ReactDOM's render -function and targets an element with the ID employee-list, in markup like this:

<h1>Employees</h1>
<div id="employee-list"></div>

The employee list element is empty by default and all rendering will take place in the browser. Now let's add display of the content in to the Twig template (that expects to have the employee data passed in as a parameter):

<div id="employee-list">
<table>
  <tbody>
  <tr>
    <th>Name</th>
    <th>Address</th>
  </tr>
  {% for employee in employees %}
    <tr>
      <td>{{ employee.name }}</td>
      <td>{{ employee.address }}</td>
    </tr>
  {% endfor %}
  </tbody>
</table>
</div>

If you now refresh the browser you will get a better experience as the page will no longer flicker as data is loaded and fetched purely on the client side. But behind the scenes React still resets and recreates the entire DOM. To change this behaviour, we will need to change call from render() to hydrate() on the client side:

ReactDOM.hydrate(<App />, document.getElementById('employee-list'));

Now if you refresh the browser you can see that everything still works as expected. But if you see your client, you will see the following message:

Warning: Did not expect server HTML to contain the text node "
    " in <div>. (warning.js:33)

This is because React does not want additional whitespace characters in the server side generated markup. With Twig we can fix this by using the spaceless tag to remove extra whitespace, which will result in the following template markup:

<div id="employee-list">{% spaceless %}
<table>
    ...our complete template, loops and all...
</table>
{% endspaceless %}</div>

Twig will now render this to a single line where the whitespace between tags are removed. Upon refresh of the browser the error will have changed to:

Warning: Expected server HTML to contain a matching <th> in <tr>.

This is simply because the two structures do not match. I intentionally left out the age column in the Twig template. Let's add the age field to both the header and the data cells to form a completely matching template:

<div id="employee-list">{% spaceless %}
<table>
  <tbody>
  <tr>
    <th>Name</th>
    <th>Address</th>
    <th>Age</th>
  </tr>
  {% for employee in employees %}
    <tr>
      <td>{{ employee.name }}</td>
      <td>{{ employee.address }}</td>
      <td>{{ employee.age }}</td> 
    </tr>
  {% endfor %}
  </tbody>
</table>
{% endspaceless %}</div>

Once this is done, upon refresh you will get no more errors and your React.js front end app is able to pick up from the existing DOM and perform whatever functionalities you wanted.

Inline styles with JSX and Twig

In React development, the latest trend has been moving to defining component styles as JavaScript within the same file. You can match these in your Twig template, but if you do go down the path of working with CSS-in-JS, it'll be a lot more work to keep the styles for both the React component as well as Twig in sync.

If you do need some inline styles or classes defined, you can still get useful debug output from React in your browsers' console, for example mismatching styles and class templates are reported like this:

Warning: Prop `style` did not match. Server: "padding:1em" Client: "background-color:grey;padding:1em"
Warning: Prop `className` did not match. Server: "heade" Client: "header"

So while inline styles are doable, I would recommend using classes and ids for styling markup that needs to be generated both with PHP and Twig on the server and with JavaScript via JSX on the browser.

Conclusion

As you can see from the example above, generating server side rendered HTML markup with Twig that React can pick up on the client side is completely feasible. Obviously you will need to maintain two sets of templates, which is not ideal.

However, the debugging messages and robustness of the React library in regards to unexpected markup from the server side makes this something that can be worked upon, and even automatically tested with of headless browsers.

Finally, server side rendering does not necessarily bring any benefits. Dashboards and admin panels and other similar applications will gain very little from this approach in practice. An area where this can be highly useful are content rich applications and CMS implementations; Here the number of different views is fairly low and the SEO benefits of SSR get to shine.

Learn more about React and SSR rendering:


Written by Jani Tarvainen on Sunday November 12, 2017
Permalink -

« Symfony Flex adoption picks up prior to release of 4.0 in November 2017 - State of GraphQL PHP libraries and Symfony integrations in 2017 »