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!