Entity view (Content)

Using React library on Drupal

By paulo
May. 3, 2018

Using React library on Drupal

In a previous post I introduced Cockpit CMS combined with React as a solution for implementing simple websites. In this post, I'll provide a different approach by experimenting the React library in Drupal as a full replacement for the Twig layer.

What is React

First of all, and to avoid a common myth, React is not a Javascript Framework, but "merely" a view library that provides a few function hooks to render HTML. It sounds quite simple, but in fact is a bit more elaborated, in the end, React is Javascript library to build user interfaces, that approaches a concept of reusable components and solve a historical issue with the slowness of the DOM by replacing it with a Virtual DOM structure.

I will not go along with all details about React as is not the purpose of this article, and you can find them on the official website.

Using it inside Drupal

I believe that one of the fundamental strengths of Drupal is the flexibility on modeling and storing structures of contents, and one of the weaknesses is on the way we consume and display those structures to the end user, mostly when the required user interactions are complex. The combination of Twig templates with jQuery is not good enough when the complexity increases, and often, we end with dozens of tweaks that are not good for the performance and code manageability. So why not forget it and make use of a modern library that is component based and provide us all modern mechanisms to build rich user experiences. Please check this project and we can see what amazing work can be built, imagine something like that powering the Drupal admin interface.

If we look at a post from the Drupal creator, we can see that some work is already in progress by the Drupal community, mostly on adopting React to rebuild the administrative UIs of Drupal (in a similar way on what Wordpress did with amazing project Calypso) and on providing the ability to be agnostic in the frontend and support multiple Javascript libraries (e.g. React, Vue.js, Ember, Angular).

Simple scenarios to use React inside Drupal

For this article, I'm just concerned about having one or more React components rendered in a Drupal page. I'm not interested in having a single React page or having a full headless solution, I just want to have the possibility to solve the complexity of some page components with React, let's call it a hybrid approach that is represented in the below picture:

react-hybrid-drupal-diagram.png

In way to do that, we have a few challenges to solve:

1. Avoid including the React library on each React component

There are many ways to start a React application, by using some boilerplates or tools like the Facebook create-react-app (https://github.com/facebook/create-react-app). Using create-react-app will help us since it removes some tricky configuration aspects and simplifies a lot the development phase. Creating a new app is a matter of running:

npx create-react-app my-app
cd my-app
yarn start

To build our app we just need to run yarn build and it will create in the build folder the transpiled JS files with all dependencies including the react and react-dom js files, the aggregated CSS files and any other referenced assets. That is ok if we are doing a single react page, but what happens if we want to render multiple react apps on the same page, we are including redundant code as the react js files are included in the build.

To avoid that we can do an eject and manipulate the create react app configurations and scripts, or use the react-app-rewired module (https://github.com/timarney/react-app-rewired) as it provides the ability to override webpack configs without the need of ejecting:

yarn add --dev react-app-rewired

After we have the module installed we can create a config-overrides.js file in the root directory and define as external the react and react-dom libs:

/* config-overrides.js */
module.exports = function override(config, env) {
// Use external version of React
 config.externals = {
   "react": "React",
   "react-dom": "ReactDOM"
 };
 return config;
}

and replace in the package.json the build command:

"build": "react-scripts build",

by

"build": "react-app-rewired build",

...so when building, the resulting main.js file will not include the react libs. 

2. Load the React component in Drupal

The create-react-app will produce a main.<hash>.js and main.<hash>.css files, we just need to expose them to Drupal in a library definition:

Assuming that we created our React app inside a custom module like react_example/js we only need to create react_example/react_example.libraries.yml with:

component:
 version: VERSION
 js:
   js/build/main.js: {}
 css:
   component:
     js/build/main.css: {}
 dependencies:
   - core/drupal

And attach our library to a component (e.g. paragraph template, field formatter, custom plugin block, twig template, etc..) that contains a div that matches the React code.

If you check again the text, you'll find that I mentioned that the create-react-app produces a main.<hash>.js and main.<hash>.css and <hash> changes on every build, so that will not work for our libraries.yml definition. That can be fixed by updating the package.json with a new build command:

   "build": "creact-app-rewired build && yarn run build:dist",
   "build:dist":
     "cd build && cp static/js/*.js main.js && cp static/css/*.css main.css",

so when running yarn build, we ensure that it will produce a main.js and main.css files without any hash in the name, and if you want we can update the script to copy the files to another folder (e.g. dist) and use another name than main.

3. Provide a mechanism to pass Drupal information as props to the react components

The mechanism in React to pass information between components is based on the concept of Props, a prop can be of a different type (e.g. object, array, string, function, etc..), so for instance, if we have a Message component that needs to render a text message in green or red (info or alert) we can pass him the text message (string) and the type (string) using props like below:

<Message text="Alert message" type="alert" />
<Message text="Info message" type="info" />

So how we can pass information from Drupal to a React component?

I figured out two different ways, the first one is based on data attributes that are present in the div element that React will handle, e.g.:

<div id="react-message-component" data-type="info" data-text="Info message"></div>

and then on the React component we just need to handle the data attributes:

import React from 'react';
import ReactDOM from 'react-dom';
import Message from './Message';

var root = document.getElementById('react-message-component');

ReactDOM.render(<Message {...(root.dataset) } />, root);

Above approach can be a bit problematic if we have complex attributes, and therefore believe the next approach is a bit better. We can set our props in a drupalSettings array and since it is defined globally we can access it from react. So when rendering our div in Drupal we can attach the settings:

$build['#markup'] = '<div id="react-message-1"></div>';

$build['#attached']['drupalSettings']['messages'][1] = [
 'text' => t('Info message'),
 'type' => 'alert',
 'element' => 'react-message-1'
];

and on React we can access to the global drupalSettings as below:

import React from 'react';
import ReactDOM from 'react-dom';
import Message from './Message';

const messages = window.drupalSettings.messages || {};

Object.keys(messages).forEach(key => {
 ReactDOM.render(
   <Message
     text={messages[key].text}
     type={messages[key].type}
   />,
   document.getElementById(messages[key].element)
 );

})

Our Message component can be something so simple as below:

import React from 'react';
import PropTypes from 'prop-types';

const Message = props => {
 return <div className={`alert alert-${props.type} fade in`}>{props.text}</div>;
};

Message.propTypes = {
 text: PropTypes.string,
 type: PropTypes.string,
};

export default Message;

4. Have a consistent mechanism to map Drupal components to React components

In our hybrid approach, we are using React for dealing with the UX complexities, but still relying on Drupal for handling the contents. We can have different scenarios, like charts, dashboards, image galleries, maps, etc.. but we need to have also the component concept on Drupal, but instead of dealing with the presentation it will deal only with the organization of contents.

And for that, I think that paragraphs module is a perfect choice, its entity based, supports different bundles, it's fieldable, supports revisions, we can handle it on views, and it can be referenced by content types or blocks so it can reside in the contents or the theme regions.

So taking into consideration our simple message example, we can have a paragraph type, named Message and composed of two fields, message, and type:

message-components-edit.png

Having it referenced in a page, by default it will output the contents of the message and type fields:

example-page-message.png

We can override the output of the paragraph in different ways, for the sake of the simplicity and to not touch any template I'll do it in a preprocess hook:

function my_module_preprocess_paragraph__message(&$variables) {
 $paragraph = $variables['paragraph'];

 // Set default content of paragraph.
 $variables['content']['#markup'] = '<div id="react-message-' . $paragraph->id() . '"></div>';

 // Attach required libraries.
 $variables['content']['#attached']['library'] = [
   'my_module/react',
   'my_module/component',
 ];

 // Set component settings into drupal settings.
 $variables['content']['#attached']['drupalSettings']['messages'][$paragraph->id()] = [
   'text' => $paragraph->get('field_message')->getString(),
   'type' => $paragraph->get('field_message_type')->getString(),
   'element' => 'react-message-' . $paragraph->id(),
 ];
}

Using above code it will output a div with the message id and the attached js libraries, so we don't need to have any field available in the paragraph default view mode and it will render:

react-message-rendered-in-page1.png

I'm aware that above is a very simple example that doesn't expose the React capabilities but at least we can see how the interaction between Drupal and React can happen. The next topic provides a real example, that makes use of advanced components like react-refetch and react-image-gallery.

Use Case - an image gallery

The React Image Gallery component provides everything that is required to build a nice looking and advanced image gallery, and it is a good example of the complex UI interactions that mentioned before. To conclude this post I decided to create a Drupal module that makes use of the React component and provides the Drupal layer that is required to manage the gallery and images.

One of the guidelines when doing the module was to avoid extra dependencies and try to use what Drupal core offers, the only dependency that wasn't avoided (therefore with some effort it could be possible) was the Paragraphs module.

The module is organized around the below features:

1. Media type

A custom media type (Image Gallery) is provided in a way to define the required attributes of the images present in the gallery.

2. Image Styles

Two image styles are provided, one for the normal gallery image and other for the thumbnail.

3. Paragraph

A paragraph that is composed only of an entity reference field (referencing the media type gallery images). I could have used only the entity reference field as it is responsible for the component rendering, but let's imagine that we need some extra fields (e.g. gallery title).

4. Custom field formatter

A field formatter that is used in the gallery entity reference field and overrides the display of the field replacing the contents with the react div and drupalSettings that will be mapped into the gallery props.

react-galleryfield-formatter-settings.png

5. A REST View

A REST View that has a paragraph id as an argument, so it can retrieve all referenced media images (using a relationship), so nothing fancy is required:

gallery-view.png

Accessing the view endpoint (/api/image-gallery/1?_format=json) will result in:

view-rest-json-result.png

6. The React App component

The react App works as a container for the react-image-gallery component and it handles the gallery attributes and the fetch of the paragraph (using react-refetch component).

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect, PromiseState } from 'react-refetch';
import ImageGallery from 'react-image-gallery';
import Loading from './Loading';
import ErrorMessage from './ErrorMessage';
import 'react-image-gallery/styles/css/image-gallery.css';

class Gallery extends Component {
  render() {
    const { imageFetch } = this.props;

    if (imageFetch.pending) {
      return <Loading />;
    } else if (imageFetch.rejected) {
      console.error(imageFetch.reason);
      return <ErrorMessage message="An unexpected error has occurred." />;
    } else if (imageFetch.fulfilled) {
      const images = imageFetch.value.map((value, idx) => {
        return {
          original: `${process.env.REACT_APP_DRUPAL_HOST}${value.image}`,
          thumbnail: `${process.env.REACT_APP_DRUPAL_HOST}${value.thumbnail}`,
          originalAlt: value.name,
          originalTitle: value.name,
          description: value.description,
        };
      });

      return <ImageGallery items={images} {...this.props.settings} />;
    }
  }
}

Gallery.propTypes = {
  id: PropTypes.string,
  settings: PropTypes.object,
  imageFetch: PropTypes.instanceOf(PromiseState).isRequired,
};

export default connect(props => {
  return {
    imageFetch: `${process.env.REACT_APP_DRUPAL_HOST}/api/image-gallery/${
      props.id
    }?_format=json`,
  };
})(Gallery);

Resuming, the module works for me as a PoC for further complex components in Drupal, as you can observe, we can separate easily the contents and the presentation without affecting too much the way we deal with Drupal, and reduce the effort on the FE by reducing the time and complexity of implementation, it also opens a door to Javascript developers that are not familiarized with Drupal, as they can build the component outside of Drupal ("yarn start" inside the js folder will permit to run the component without Drupal running, check the README.md file for more details).

The following screencast illustrates how the module work:

The module is available on GitHub