Drupal 8 and React: A Progressive Enhancement!

When the conversation of decoupled comes up, a lot of times it revolves around chopping the entire front end off the site and replacing it with something like React or Vue. This is not only a major task for a development team but it could also be a hard sale to clients, since it is going to impact future improvements and possibly take longer to initially develop. 

But that doesn’t mean we can’t start adding our favorite JS framework into existing sites and showing off the cool interactions React/Vue/Angular give us. 

The Fast Way

When prototyping something internally or for a client, I usually use the following setup. It has a number of drawbacks, but it is fast. Using Create React App and Drupal’s Block Class plugin, I just make a block with the div I’m targeting. In my Block class I just change the markup to something like:

/**
   * {@inheritdoc}
   */
  public function build() {
    return array(
      '#markup' => '<div id="react-root"></div>',
    );
  }

And then in my Create React App, edit the index.js file with the following:

ReactDOM.render(<App />, document.getElementById('react-root'));

From here I can build out my React app and it will load in that div. The biggest issue I have with this is the page source will show an empty div on load and then populate after the JS has time to run and make any queries to whatever API endpoint I’m using, either external or from my Drupal site. 

This approach combined with a CSV migration of some data into your Drupal site and a charting program like Google Charts (the React version) can create some impressive visuals quickly. 

React Hydration

In the newest versions of React, ReactDOM got a new toy, hydrate. Now you can either call render() or hydrate() when building a new app. When I saw this new method, I was super excited. In the past to try and get server rendered code outside of a Node.js environment, I tried integration PHPV8 and all kinds of other hacks to server render my initial load of the React app. 

Render vs Hydration

So what is the difference in these two methods. Render will effectively replace what is in the targeted DOM element with the new React app, attaching all the event listeners to the new rendered app. 

Hydration on the other hand, tells React to look at the markup as it’s already been rendered and attach the event listeners to the code. This is pretty easy in a Node environment as I can run the same React code on the server and in the client to make sure it always matches up. But I have to be extra careful of my Drupal markup to make sure it matches exactly what my React app is going to render. 

ReactDOM documentation

A simple counter example

Github Repo: https://github.com/dgading/d8_react_hydrate_example

To try and test this, I built a very simple block example. I hope to expand into something bigger using something like Views, but that is for a future project. 

The Module

The module itself is very simple, an info file, a libraries file, a JavaScript file, and a Block plugin file. 

The Info File

The info file is the only file needed to enable a module on your Drupal site. I’m not doing anything custom in this file or requiring any additional modules. 

name: Drupal 8 and React Integration
description: Examples of using React hydrate and Drupal 8 together.
package: Custom

type: module
core: 8.x

The Libraries File

The libraries file in the module is where we can add our custom JS and the required React files. I used this React doc as my inspiration on which files to load.  

react:
  version: 1.x
  js:
    https://unpkg.com/react@16/umd/react.development.js: {type: external, minified: true}
    https://unpkg.com/react-dom@16/umd/react-dom.development.js: {type: external, minified: true}

countdown:
  version: 1.x
  js:
    js/countdown.js: {}
  dependencies:
- core/drupalSettings

To breakdown the file above, I am loading two separate libraries. The “react” library has all the files from the React docs and my “countdown” library adds my custom JS code and also requires drupalSettings from Drupal core. I’m using drupalSettings to help me pass variables from my PHP code to my React as initial props. More on that in a bit. 

I separated this into two libraries so I could create more examples and include React and not have to also include my countdown code. 

The Block

Before I go through the React code, I want to quickly explain the Block I am creating. Much like the quick example above, I need to create a DOM element that I can load React into. The extra piece here is I want to also add some data from server to the element and pass that same data to my React app. This should probably be done with a Twig template and some variables, but I am still just prototyping, so I hardcoded the markup. 

<?php

namespace Drupal\d8_react_hydrate_example\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Countdown' Block.
 *
 * @Block(
 *   id = "countdown_block",
 *   admin_label = @Translation("Countdown"),
 *   category = @Translation("Custom"),
 * )
 */
class CountdownBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    $nid = 0;
    $node = \Drupal::routeMatch()->getParameter('node');
    if ($node instanceof \Drupal\node\NodeInterface) {
      $nid = $node->id();
    }
    
    return [
      '#markup' => '<div>NID: '. $nid . '</div><div id="countdown-app"><h1>'. $nid . '</h1></div>',
      '#attached' => [
        'library' => [
          'd8_react_hydrate_example/react',
          'd8_react_hydrate_example/countdown'
        ],
        'drupalSettings' => [
          'reactNID' => $nid,
        ]
      ],
    ];

  }

}

In the build function for this block there are two things happening, we are setting a variable to the current node id and we are returning our markup and attaching our JS to the block. Finally we are passing the node id to drupalSettings so we have it available for our React app. 

The React App

Finally the React app. This is what we’ve been waiting for. Instead of building an entire Webpack build system, I am using the instructions from the React docs for adding React to an HTML file. 

const domContainer = document.querySelector('#countdown-app');
const e = React.createElement;

class Countdown extends React.Component {

  constructor(props) {
    super(props);
    this.state = { counter: props.reactNID };
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000,
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    const currentCount = this.state.counter;
    if (currentCount > 0) {
      this.setState({
        counter: currentCount - 1,
      });
    } else {
      clearInterval(this.timerID);
    }
  }

  render() {
    return e(
      'h1',
      {},
      this.state.counter,
    );
  };
}

ReactDOM.hydrate(e(Countdown, {reactNID: window.drupalSettings.reactNID}, null), domContainer);

In the code above, we are rendering a React component that takes one prop, reactNID, and load it into state. Once it is in our state, we render it in our h1 element and after the component mounts we decrement the number by one each second. When it reaches 0, we unmount the component. 

ReactDOM.hydrate(e(Countdown, {reactNID: window.drupalSettings.reactNID}, null), domContainer);

This is our code to build the app. The hydrate method is loading our component, setting our intial prop by looking at the drupalSetting variable we passed to window in our Block code, and targeting our DOM element to load the app into. Since window.drupalSettings.reactNID and our markup from the Block build function are identical, React will start the countdown like it built the the app itself. 

Conclusion

While this isn’t a very complex example, I feel it is a good place to start exploring how to add a modern interaction layer to your Drupal site without completely decoupling. 

I am also writing up a similar pattern for WordPress which I hope to have finished soon.