Drupal and React/TypeScript components
How to easily integrate a React/TypeScript component in Drupal 8.
Drupal
with React
/TypeScript
components is a great combination, and it's not so hard to make these guys work together.
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
- Create a custom module
- Build a Drupal library
- Create a field formatter
- Build the React component
- Install and enjoy
- 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.
Now to test it, let’s add some text in our text field.
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.
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!