How to use Drupal 8 as a backend for React.js (Drupal 8, ES2015 (w/ Babel) and React.js)

Today I'm going to show you how to display a Drupal 8  block with recent comments. Displayed dynamically using React.js.

You can see it in action below:

Drupal 8 React.js see it in action

I have a vanilla Drupal site installed using the Standard profile so I have the Article content type with comments, which I'm going to use for this project.

Before we go further let's look at the prerequisites:

Let's get started.

First I'm going to create the folder called drupal_block_reactive inside my modules/custom folder, then I'll put my .info.yml file inside the folder.

drupal_block_reactive.info.yml

name: Recent comments (React)
type: module
description: 'Latest comments block with React.js'
package: Development
version: '8.x-1.x-dev'
core: '8.x'
dependencies:
  - block
  - comment
  - node
  - rest
  - user

I have Rest module as a required module here since I'm going to crate a View with the Rest display to get comments so note that the urls will be "/api/comments". So this will be the only connection to fetch data from the backend.

Now let me add my Views export here so you can import it using Drupal 8 configurations import.

views.view.recent_comments_react.yml

langcode: en
status: true
dependencies:
  module:
    - comment
    - node
    - rest
    - user
id: recent_comments_react
label: 'Recent comments react'
module: views
description: 'Recent comments.'
tag: default
base_table: comment_field_data
base_field: cid
core: 8.x
display:
  default:
    display_plugin: default
    id: default
    display_title: Master
    position: 0
    display_options:
      access:
        type: perm
        options:
          perm: 'access comments'
      cache:
        type: tag
      query:
        type: views_query
      exposed_form:
        type: basic
      pager:
        type: some
        options:
          items_per_page: 10
          offset: 0
      style:
        type: html_list
        options:
          grouping: {  }
          row_class: ''
          default_row_class: true
          type: ul
          wrapper_class: item-list
          class: ''
      row:
        type: fields
        options:
          default_field_elements: true
          inline:
            subject: subject
            changed: changed
          separator: ' '
          hide_empty: false
      relationships:
        node:
          field: node
          id: node
          table: comment_field_data
          required: true
          plugin_id: standard
      fields:
        subject:
          id: subject
          table: comment_field_data
          field: subject
          relationship: none
          group_type: group
          admin_label: ''
          label: ''
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: false
            ellipsis: false
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          click_sort_column: value
          type: string
          settings:
            link_to_entity: false
          group_column: value
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
          plugin_id: field
          entity_type: comment
          entity_field: subject
        changed:
          id: changed
          table: comment_field_data
          field: changed
          relationship: none
          plugin_id: field
          group_type: group
          admin_label: ''
          label: ''
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: true
            ellipsis: true
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          type: timestamp_ago
          settings:
            future_format: '@interval hence'
            past_format: '@interval ago'
            granularity: 2
          entity_type: comment
          entity_field: changed
        cid:
          id: cid
          table: comment_field_data
          field: cid
          relationship: none
          group_type: group
          admin_label: ''
          label: ''
          exclude: false
          alter:
            alter_text: false
            text: ''
            make_link: false
            path: ''
            absolute: false
            external: false
            replace_spaces: false
            path_case: none
            trim_whitespace: false
            alt: ''
            rel: ''
            link_class: ''
            prefix: ''
            suffix: ''
            target: ''
            nl2br: false
            max_length: 0
            word_boundary: true
            ellipsis: true
            more_link: false
            more_link_text: ''
            more_link_path: ''
            strip_tags: false
            trim: false
            preserve_tags: ''
            html: false
          element_type: ''
          element_class: ''
          element_label_type: ''
          element_label_class: ''
          element_label_colon: false
          element_wrapper_type: ''
          element_wrapper_class: ''
          element_default_classes: true
          empty: ''
          hide_empty: false
          empty_zero: false
          hide_alter_empty: true
          click_sort_column: value
          type: number_integer
          settings:
            thousand_separator: ''
            prefix_suffix: false
          group_column: value
          group_columns: {  }
          group_rows: true
          delta_limit: 0
          delta_offset: 0
          delta_reversed: false
          delta_first_last: false
          multi_type: separator
          separator: ', '
          field_api_classes: false
          entity_type: comment
          entity_field: cid
          plugin_id: field
      filters:
        status:
          value: true
          table: comment_field_data
          field: status
          id: status
          plugin_id: boolean
          expose:
            operator: ''
          group: 1
          entity_type: comment
          entity_field: status
        status_node:
          value: true
          table: node_field_data
          field: status
          relationship: node
          id: status_node
          plugin_id: boolean
          expose:
            operator: ''
          group: 1
          entity_type: node
          entity_field: status
      sorts:
        created:
          id: created
          table: comment_field_data
          field: created
          relationship: none
          group_type: group
          admin_label: ''
          order: DESC
          exposed: false
          expose:
            label: ''
          plugin_id: date
          entity_type: comment
          entity_field: created
        cid:
          id: cid
          table: comment_field_data
          field: cid
          relationship: none
          group_type: group
          admin_label: ''
          order: DESC
          exposed: false
          plugin_id: field
          entity_type: comment
          entity_field: cid
      title: 'Recent comments'
      empty:
        area_text_custom:
          id: area_text_custom
          table: views
          field: area_text_custom
          relationship: none
          group_type: group
          admin_label: ''
          label: ''
          empty: true
          content: 'No comments available.'
          tokenize: false
          plugin_id: text_custom
      display_extenders: {  }
    cache_metadata:
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - user.permissions
      max-age: -1
      tags: {  }
  rest_export_1:
    display_plugin: rest_export
    id: rest_export_1
    display_title: 'REST export'
    position: 3
    display_options:
      display_extenders: {  }
      path: api/comments
      style:
        type: serializer
        options:
          uses_fields: true
          formats:
            json: json
      filters: {  }
      defaults:
        filters: false
        filter_groups: false
      filter_groups:
        operator: AND
        groups:
          1: AND
      row:
        type: data_field
        options:
          field_options:
            subject:
              alias: ''
              raw_output: false
            changed:
              alias: ''
              raw_output: false
    cache_metadata:
      max-age: -1
      contexts:
        - 'languages:language_content'
        - 'languages:language_interface'
        - request_format
        - user.permissions
      tags: {  }

Okay the backend if ready now, let's add the library dependencies, so in Drupal 8 how do we do that is we use .libraries.yml to add any js dependencies.

drupal_block_reactive.libraries.yml

reactjs:
  remote: https://github.com/facebook/react
  version: 15.1.0
  license:
    name: BSD
    url: https://github.com/facebook/react/blob/master/LICENSE
    gpl-compatible: true
  js:
    # Non-minified version: development friendly, debugging is possible.
    //cdn.jsdelivr.net/react/15.1.0/react.js: { type: external, minified: false }
    //cdn.jsdelivr.net/react/15.1.0/react-dom.js: { type: external, minified: false }

component:
  version: VERSION
  js:
    js/dist/index.js: {}
  dependencies:
    - core/drupal

So what this code does is, this will get the React.js 15.1.0 added together with component which is what I'm going to detail next.

React.js Components let you split the UI into independent, reusable pieces, so each comment will be a Component and Comment container will be another Component.

I'm gonna create two files for those two components inside js/src/components directory.

js/src/components/Comment.js

// Comment component.
class Comment extends React.Component {
  render() {
    return ( <div className = "Comment" ><span> {this.props.subject} < /span> | <span>{this.props.changed}</span ></div>);
  }
}

export default Comment

js/src/components/CommentBox.js

import Comment from './Comment'

// CommentBox component.
class CommentBox extends React.Component {
  constructor() {
      super();
      // Setting initial state.
      this.state = {
        comments: []
      }
    }
    // Data from a service.
  _fetchData() {
      jQuery.ajax({
        url: this.props.url,
        dataType: 'json',
        success: (comments) => this.setState({
          comments
        }),
        error: (xhr, status, err) => {
          console.error(this.props.url, status, err.toString());
        }
      });
    }
    // Gets data from state, returns a list components.
  _getComments() {
    // Get the list of comments from the state.
    const commentsList = this.state.comments;
    // Return an array of sub-components.
    if (commentsList.length > 0) {
      return commentsList.map((comment) => {
        return <Comment subject = {comment.subject} changed = {comment.changed} key = {comment.cid}/>
      });
    }
    return ( <p> {Drupal.t('No comments.')} </p>
    );
  }
  componentDidMount() {
    this._fetchData();
    setInterval(this._fetchData.bind(this), this.props.timeInterval);
  }
  render() {
    const commentsNodes = this._getComments();
    return ( <div className = "CommentBox" > {commentsNodes} </div>
    );
  }
}

export default CommentBox

Now we have components added, let's put them together in the gatekeeper index.js file.

js/src/index.js

/**
 * @file
 * Attaching React component via Drupal behaviors..
 */

import CommentBox from './components/CommentBox'

Drupal.behaviors.drupal_block_reactive = {
  attach: (context) => {
    // Render our component.
    ReactDOM.render(
      <CommentBox url='/api/comments' timeInterval={2000}/>,
      document.getElementById('recent-comments-react')
    );
  }
};

Now let's add the package.json for the dependencies.

js/package.json

{
  "name": "drupal-block-reactive",
  "version": "1.0.0",
  "description": "A drop of reactivity in Drupal 8",
  "main": "gulpfile.js",
  "private": true,
  "author": "Kalin Chernev",
  "url": "https://github.com/kalinchernev",
  "repository": {
    "type": "git",
    "url": "https://github.com/kalinchernev/drupal_block_reactive.git"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.5.0",
    "gulp": "3.9.1",
    "browserify": "13.1.0",
    "gulp-babel": "6.1.2",
    "vinyl-source-stream": "1.1.0",
    "babelify": "7.3.0"
  }
}

js/gulpfile.js

I'm going to use Gulp to compile the JS.

const gulp = require('gulp');                 
const babel = require('gulp-babel');          
const browserify = require('browserify');     
const source = require('vinyl-source-stream');  
                                            
gulp.task('default', () => {                  
  return browserify({                       
      entries: 'src/index.js',                
      extensions: ['.jsx'],                 
      debug: true                           
  })                                        
  .transform('babelify', {                  
      presets: ['es2015', 'react']          
  })                                        
  .bundle()                                 
  .pipe(source('index.js'))                  
  .pipe(gulp.dest('dist'));                 
});

Now let's connect this JS in Drupal UI using a static HTML block.

src/Plugin/Block/RecentCommentsBlock.php

<?php

namespace Drupal\drupal_block_reactive\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Recent comments (reactive)' block.
 *
 * @Block(
 *   id = "recent_comments_reactive",
 *   admin_label = @Translation("Recent comments (reactive)"),
 *   category = "Development"
 * )
 */
class RecentCommentsBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    return [
      '#markup' => '<div id="recent-comments-react"></div>',
      '#attached' => [
        'library' => [
          'drupal_block_reactive/reactjs',
          'drupal_block_reactive/component',
        ],
      ],
    ];
  }

}

Now everything is set up, you can go to the js folder and run npm install to install all the dependencies. Then run gulp to compile the JS.

Once those steps are completed you can enable the block in a sidebar or content region and add couple of comments to see it in action.

You can also find the complete code here in this GitHub repository.