Today, Drupal developers must acknowledge that when building a rich, stateful user-facing interface, it is best to use a modern JavaScript framework. Although less framework-driven JavaScript approaches like vanilla JavaScript and jQuery were good enough for many years, I have started to realize that my criteria for whether a piece of UX “should use a framework” has gotten pretty darn low.

Fortunately, even if Drupal is serving your site’s pages, it is possible to make use of React to build components. Some people refer to this method as “progressive decoupling.”

Selecting a Framework, or No Framework

Although vanilla JavaScript or jQuery is fine for small UI behavior implementations, I almost always select React for “centerpiece” user experiences. Here’s a couple of heuristics I use to decide whether employing a framework is worthwhile:

Am I using Drupal #ajax to manage more than a couple of states for end-user UX?

If so, I typically go with a React block. The #ajax API has been a workhorse for building a consistent editor/admin UX for Drupal, but it is just too slow to provide a good multi-state UX for visitors/end-users. This is because when a page uses the #ajax form API, you have to contact the server and reprocess the form for any kind of state change, even if there is no real reason to do so.

Does the page in question need to have many different React components?

This can make embedding react components more complex. Although a clever webpack and Drupal frontend library configuration can allow you to efficiently include multiple react components on a single page, it requires a more extensive setup. This can tip things in favor of staying with vanilla JavaScript or jQuery for a particular component if it might share a page with lots of other small components.

Building your React Block

You’ve decided to use React for a cool, stateful user interface. Congrats! I will show a couple of ways to make it happen.

Don’t use create-react-app

If you’re not familiar with create-react-app, it is a nice tool that generates scaffolding for a React application. It is also not great for building React blocks that you plan to embed in Drupal since these types of React applications are less complex. For instance, you do not need the public folder that create-react-app provides, nor do you need a lot of its dependencies. Some of these can interfere with being able to use React developer tools and can cause other confusion too.

What to do

It turns out it is not very difficult to generate your own react project with just the dependencies you need.

First, decide where you want your React component to live in your codebase. I usually put it into a custom module, because we need to write server-side code to embed the component. You could also put the React component into the theme.

Let’s say we want to use a custom module as the location, for now. I usually make a js/react/appname folder. Do this as you see fit, and move to that folder using the CLI. Then, let’s get started by initializing a new project.

npm init

 

This will generate a basic package.json and some scaffolding.

Next, you’ll want to set a node version. For this, I recommend nvm. You probably want the latest stable version of node:

nvm install node
node -v > .nvmrc

 

Now it’s time to install the required packages. Here is a summary of what you’ll need:

Package name

ID

Description

Dependency Type

Babel Core

@babel/core

Babel is a JavaScript transpiler that converts the JavaScript that you write (e.g. React) into JavaScript that can run in any browser.

dev

Babel CLI

@babel/cli

CLI package for Babel.

dev

Babel Preset Env

@babel/preset-env

The basic Babel JavaScript transpilation package

dev

Babel Preset React

@babel/preset-react

Babel transpilation for React JavaScript

dev

Babel Loader

babel-loader

Allows webpack to use Babel as a transpiler.

dev

Webpack

webpack

A tool for bundling JavaScript so that a browser can use it.

dev

Webpack CLI

webpack-cli

Allows npm to run webpack commands

dev

React

react

The React JavaScript library

prod

React Dom

react-dom

The entry point to the DOM for React

prod

Dependency type refers to whether the module is needed for the actual production build that end users will be interacting with. 

To install these packages, start with dev dependencies:

npm install --save-dev @babel/cli  @babel/core @babel/cli @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli

 

Then, install production dependencies

npm install react react-dom

Add a webpack config

If you’ve worked with React before, it’s likely you’ve used webpack to build your project. The webpack config needed here is pretty basic:

const path = require('path');
 
const config = {
 entry: './src/index.js',
 devtool: (process.env.NODE_ENV === 'production') ? false : 'inline-source-map',
 mode: (process.env.NODE_ENV === 'production') ? 'production' : 'development',
 output: {
   path: path.resolve(__dirname, 'dist'),
   filename: 'app.bundle.js'
 },
 module: {
   rules: [
     {
       test: /\.js$/,
       exclude: /(node_modules)/,
       use: {
         loader: 'babel-loader'
       }
     }
   ]
 },
};
 
module.exports = config;

 

This goes in a file called webpack.config.js. I’ll explain the important parts:

Entry

In webpack, the entry point is the file that imports all of your other packages, at the top of your application’s tree. If you’re dealing with React, this is the file where you use react-dom to attach your react script to a DOM element.

Devtool

Based on whether we’re doing a production or development build, we tell the bundle to use a source map. This bloats the bundle a lot so you don’t want to add a source map to the prod build. The NODE_ENV comparison comes into play later when we have npm run our webpack script.

Output

The path to your bundle, containing the whole app (e.g. all the React components and imports you need). Usually this is in a “dist” folder.

Module

This tells webpack to use babel for transpiling your JS so that the browser can run React.

Configure package.json

There are two things you need to do:

Set up your scripts

You’ll need to add the following to the top level of package.json:

 "scripts": {
   "build": "NODE_ENV=production webpack",
   "watch": "webpack --watch --progress"
 },

This tells webpack to either do a development (watch) or a production (build) build. As we noted earlier, we use an environment variable to establish whether to use a source map (which bloats the package but facilitates debugging).

Set up Babel

Another role that package.json plays is to let you configure how Babel works. You have to add this:

 "babel": {
   "presets": [
     [
       "@babel/preset-env",
       {
         "targets": {
           "browsers": [
             "IE >= 11",
             "last 3 versions"
           ]
         }
       }
     ],
     "@babel/preset-react"
   ]
 },

Build a "Hello World" React App

Below is a simple React app that will let you test things. Put this in src/index.js:

import React from 'react';
import { render } from 'react-dom';
 
const Root = () => {
 return (
   <>Hi there</>
 )
}
 
render(<Root/>, document.querySelector('#my-app-target'));

 

Nothing fancy.

Include your React app in Drupal

We have to tell Drupal to include our React app on a page. Here are the basic steps:

  • Define a library for your React app.
  • Include the React app’s target markup on a page.
  • Include the React app’s Drupal library on a page.
  • (Optional) Pass data to the app via drupalSettings.

Let’s break this down a bit:

Define a library for your React app

In your custom module, add a mymodule.libraries.yml file as follows:

my-app:
 version: 1.x
 js:
   js/react/my-app/dist/app.bundle.js: { minified: true }

 

This will let us load the library in an #attached property or in a Twig template.

Include the React app’s target markup on a page / Include the React app’s Drupal library on a page

There are quite a few ways to do this. I think the absolute simplest way is to put the following in the twig template where you want to load your React app:

{{ attach_library('mymodule/my-react-app') }}
 
<div id="my-app-target"></div>

 

If we do this, the React app will put itself within the my-app-target div.

You can also use a render array to embed the app, if you want to do things from a form alter for instance:

$form['my_react_app'] = [
 '#markup' => '<div id="my-app-target"></div>',
 '#attached' => [
   'library' => [
     'mymodule/my-react-app'
   ],
 ],
];

 

Let’s run it!

If you are following this as a tutorial, this is a good time to test things out. First, let’s test a production build:

npm run build

 

In the browser, load the page that is running your twig template from the last step. You should see the “Hi there” text in the target div.

(Optional) Pass data to the app via drupalSettings

A common use case is having a setting in Drupal (a checkbox on a paragraph entity, or some global variable) that we need to impact the way the React app works. We use the #attached/drupalSettings API to accomplish this.

Add drupalSettings dependency to Library

First, we need to modify our library definition as follows:

my-react-app:
 version: 1.x
 js:
   js/react/my-app/dist/app.bundle.js: { minified: true }
 dependencies:
   - core/drupalSettings

Set drupalSetting value and provide to browser

Now, we need to pass the setting to the browser using the #attached framework. The details here depend on what part of Drupal we are working within. Generally you’ll need to work in either a preprocess hook or some type of callback that can change the render array. The #attached bits look the same regardless.

Here’s an example of the preprocess method:

function mymodule_preprocess_node(&$variables) {
 $variables['content']['#attached']['drupalSettings']['myReactApp']['mySetting'] = 'some value';
}

 

Or, if you are modifying the render array (forms, build layer etc):

$form['my_react_app'] = [
 '#markup' => '<div id="my-app-target"></div>',
 '#attached' => [
   'library' => [
     'mymodule/my-react-app'
   ],
   'drupalSettings' => [
     'myReactApp' => [
       'mySetting' => 'some value',
     ],
   ],
 ],
];

 

In this case, we add the drupalSetting in the same place where we add the markup and the library.

Access drupalSettings value in the React App

Luckily, drupalSettings is a global variable so you can use it directly in your React app:

import React from 'react';
import { render } from 'react-dom';
 
const Root = () => {
 return (
   <>Hi there. My setting is {drupalSettings.myReactApp.mySetting}</>
 )
}
 
render(<Root/>, document.querySelector('#my-app-target'));

Conclusion

I hope this post has helped you add an embedded React app to your Drupal 8 site!