Drupal and React/TypeScript components

How to easily integrate a React/TypeScript component in Drupal 8.

Marco Martino
Soulweb Academy
7 min readMar 11, 2021

--

Drupalwith React/TypeScript components is a great combination, and it's not so hard to make these guys work together.

Drupal and React/TypeScript
Drupal and React/TypeScript

In this article we’ll go a step further: we are going to see how to build a fully-decoupled component integrated in Drupal, with React and TypeScript.

We will use some other tools along the way, one of them being Webpack, that will compile our code for the React component into a single .js file, to be easily attached to Drupal as a library. No panic, we'll talk about each of them at the right moment ;).

There are several ways to integrate them together, in this case scenario we are going to build a custom module that will host a custom field formatter with a React component interface.

Summary

  1. Create a custom module
  2. Build a Drupal library
  3. Create a field formatter
  4. Build the React component
  5. Install and enjoy
  6. One last thing

Create a custom module

First things first: We need a custom Drupal module to host all our work. You can go the old-fashioned way of creating a new module from scratch or take the shortcut and generate the skeleton with Drupal Console:

$ drupal generate:module  --module "Drupal and React" --machine-name="drupal_and_react"

Choose whatever option you prefer.

Build a Drupal library

At this point we are ready to create a Drupal library to load the .js file of the React component that we'll build later.

To achieve this, we need to add the Drupal hook hook_library_info_build() to the generated drupal_and_react.module file:

Let’s have a closer look at it.

As you can see, we are defining two versions of our component, one for development and another one for production. This is useful so we can have different configurations for both environments and optimize each one for its purpose.

$js_dev = 'assets/js/dist/app.bundle.js';
$js_prod = 'assets/js/build/app.bundle.js';

We will rely on the environment variable APP_ENV to load one or the other version.

$js_src = ( isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'production' ) ? $js_prod : $js_dev;
$js_src = file_exists($module_path . '/' . $js_src) ? $js_src : $js_prod;

In the next fragment of the code, we set the options for our library and generate our library called app_bundle.

$js = [];
if ( file_exists($module_path . '/' . $js_src) ) {
$js[$js_src] = [
'minified' => TRUE,
'preprocess' => FALSE,
];
$libraries = [
'app_bundle' => [
'version' => 'VERSION',
'js' => $js,
],
];
}
else {
throw new FileNotFoundException(
$module_path . '/' . $js_src
);
}
return $libraries;

Note the FileNotFoundException exception at the end to cover the case that the app has never been built, neither in dev nor prod version.

Once done this, Drupal knows how to load our JS app as a library. We don’t have our app bundle file yet — we’ll work on it in a while — but first we need a way to attach our library to some Drupal content, and for this we are going to use a custom field formatter.

Create a field formatter

Using a field formatter to attach the library is — maybe — a little more complex than using a block or a controller, but it has the advantage to be a more flexible solution to get our React app running. Anyway, the process for any other Drupal component (block/controller/field/etc.) is the same, you need to attach the component library to the render array (as shown below).

Again, we can generate the skeleton for our field formatter with drupal-console. Set the required information as you like, we will use string as field type for this example, being it enough for our purpose.

$ drupal generate:plugin:fieldformatter

Now we are going to work on the viewElements() method:

Again, let’s dig into it.

We get some info from the entity and the field to generate a unique id for our div. This is to avoid the case that more components from different fields on the same page try to apply to the same DOM element.

$id = $items->getEntity()->id();
$field_name = $this->fieldDefinition->getItemDefinition()->getFieldDefinition()->getName();
$wrapper_id = 'drupal-and-react-app-' . $field_name .'-'. $id;
$build = [
'#markup' => '<div id="' . $wrapper_id . '"></div>',
];

Now, we can attach the library to the render array of our field formatter:

$build['#attached']['library'] = [
'drupal_and_react/app_bundle',
];

And finally we can pass the required data to our React component via Drupal settings.

$build['#attached']['drupalSettings']['drupal_and_react_app'][$id] = [
'id' => $id,
'wrapper' => $wrapper_id,
'content' => $elements,
];
return $build;

Our job is pretty done on the Drupal side. Yeah! 🎉 Let’s switch to our React component and play with it!

Build the React component

Make sure to have npm installed.

The first step will be set up the environment for our app.

In the files structure of our module, add the directory assets/js like this:

_your_drupal_root_dir_/modules/custom/drupal_and_react/assets/js

From inside this new created directory, run $ npm init to generate the package.json file.

There’s a bunch of dependencies we have to add now to it to be able to create our component.

React of course:

$ npm i react react-dom

It’s time for TypeScript support, we install the typescript package together with the types for the two React related packages we just called in:

$ npm i --save-dev typescript @types/react @types/react-dom

Next step is to add some configuration for TypeScript in its dedicated file called tsconfig.json that has to live in the root directory of our app.

tsconfig.json

{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "ES6",
"jsx": "react",
"typeRoots": ["./src/@types", "./node_modules/@types"]
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}

As we said, we will use Webpack - and some loaders - to convert our code to a single .js file, so let's do it now.

$ npm i --save-dev webpack webpack-cli css-loader file-loader style-loader source-map-loader ts-loader

And add a configuration file for it, webpack.config.js where we will specify the behavior we want.

webpack.config.js

const path = require("path");
const isDevMode = process.env.APP_ENV !== "production";
const config = {
entry: {
main: ["./src/index.tsx"]
},
devtool: isDevMode ? false : "source-map",
mode: isDevMode ? "development" : "production",
output: {
path: isDevMode
? path.resolve(__dirname, "dist")
: path.resolve(__dirname, "build"),
filename: "app.bundle.js"
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"]
},
module: {
rules: [
{
test: /\.(t|j)sx?$/,
enforce: "pre",
use: [
{
loader: "ts-loader"
}
],
exclude: /node_modules/
},
{
enforce: "pre",
test: /\.js$/,
exclude: /node_modules/,
loader: "source-map-loader"
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|gif)$/,
loader: 'file-loader',
options: {
outputPath: 'images',
publicPath: (url, resourcePath, context) => {
const modulePath = context.substr(context.indexOf('/modules/', -1));
const relativePath = isDevMode
? path.join(modulePath, "dist")
: path.join(modulePath, "build");
return path.join(relativePath, 'images', url);
},
},
},
]
}
};
module.exports = config;

This contains the settings about the output file and the extensions it will use.

Again we refer to APP_ENV to switch between the development and the production builds and, as you can see, it defaults to development mode until we set it specifically as "production".

Everything is (almost) ready now. But before proceeding to code on our app, we still need to add the instructions to run Webpack into our package.json file.

...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "APP_ENV=production webpack",
"build:dev": "webpack",
"start": "webpack --watch --progress"
}
...

With this new lines added to the scripts section, we define how to build the component for production and development mode, but also - and it's our best friend for development - how to start a live watching process that will re-build the package everytime we save some change on our files.

OK, time to create our component!

If you are familiar with React and Typescript development there's not so much to comment here...

…but it’s worth notice how, as you can see, we get the data from Drupal in drupalSettings so we are ready to format and display them as we like.

const drupalAndReactApp: Settings =
window.drupalSettings.drupal_and_react_app || {};

If everything is correct, we should be able to launch our watcher and generate our first build.

$ npm start

Install and enjoy

It’s time to install our module in Drupal now and see the result of our work.

You can find and enable it in the list of available modules at http://YOUR_WEBSITE_DOMAIN/admin/modules or enable it with drush:

$ drush en drupal_and_react

After that, we need to apply our field formatter to some text field. We created a dedicated one just for the purpose.

Change the field formatter in Display options
Change the field formatter in Display options

Now to test it, let’s add some text in our text field.

Text field to be rendered by React app
Text field to be rendered by React app

And finally, we are done! 🎉🎉🎉🎉

As you can see the entered text “React will render this text” is contained in a div that has the ID that we defined in our formatter, and the element has the class 'react-text', so we are sure that it's coming from our component.

Text rendered by React app
Text rendered by React app

One last thing

Our package.json contains the following scripts to build our components.

...
"build": "APP_ENV=production webpack",
"build:dev": "webpack",
"start": "webpack --watch --progress"
...

We have already seen npm start. Run npm run build:dev to just build the development component. Run npm run build to build the production component.

The scope of this article is mostly to show how to integrate your React component into Drupal in the easiest way.

But we have more advanced material to share (e.g. Drupal + React + GraphQL, Drupal + React + EsLint + Prettier) in other stories to come. We really hope you could find this story useful and please, follow us to get the next updates!

--

--

Marco Martino
Soulweb Academy

Software Engineer, Open Source enthusiast, Full-stack Developer, Drupal specialist, React Developer, GIS Developer, Data lover