Decoupled Drupal with React

Preston So

prestonso@prestonsopreston.so

Welcome

Preston So has designed and developed websites since 2001 and built them in Drupal since 2007. He is Development Manager of Acquia Labs at Acquia and co-founder of the Southern Colorado Drupal User Group (est. 2008). Previously, Preston was Technical Lead of Entertainment Weekly at Time Inc.

What we'll cover

  1. What is decoupled Drupal?
  2. What is React?
  3. React architecture and components
  4. React and decoupled Drupal
  5. React and the Drupal ecosystem

What is decoupled Drupal?

  • Background and history
  • What is decoupled Drupal?
  • Decoupled Drupal 7 and 8
  • Setting up RESTful Drupal

A brief history of content management

  • Content management systems like Drupal are traditionally monolithic.
  • In traditional monolithic CMS architectures, the CMS provides soup-to-nuts feature sets.
  • This includes server-side templating, and all markup is controlled by a server-side language.

From Web 1.0 to Web 3.0

  • Web 1.0: Flat front-end assets uploaded via FTP, fetched by browser.
  • Web 2.0: Content management system provides a server-side template engine which couples data with markup rendering. The server side provides all data.
  • Web 3.0: A RESTful API serves as the mediator between server side and client side (often after initial page load). The client side asks for data through XMLHttpRequest, and the RESTful API serves a (JSON) payload.

What is decoupled Drupal?

  • Simply put, decoupled Drupal is the use of Drupal as a data provider by means of a RESTful API. There are two types of architectures in the wild which use this architecture.
  • In fully decoupled Drupal, Drupal serves solely as a JSON API which serves data payloads to other applications. A client-side framework (often shared isomorphically client-server) controls all rendering.
  • In progressively decoupled Drupal, Drupal controls some of the render to provide markup within a single application. JavaScript then takes over client-side rendering.

Problems of decoupled Drupal

Much of Drupal's robustness is lost.

  • Cross-origin requests, security, authentication, and passwords
  • Form validation
  • Content workflow and management
  • Layout and display management
  • Multilingual and localization
  • Accessibility and user experience

Decoupled Drupal 7

  • Services, a contrib module, is not strictly RESTful but provides web services for Drupal 7 (8 in the works).
  • Built-in REST and XML-RPC interfaces
  • Exposes content entities at custom endpoints
  • Services Entity extends Services to work with all entity types
Where's the guy using REST correctly?

Source: GraphQL with Nick Schrock, React London meetup (10/15)

Decoupled Drupal 7

  • restWS exposes any Drupal entity on its existing path based on headers.
  • Restful exposes entities whose response data developers can customize.

Decoupled Drupal 8

  • WSCCI (Web Services and Context Core Initiative) incorporated Web Services into Drupal 8 core.
  • The core REST modules allow for all content entities to be exposed as JSON+HAL, and Views natively supports “REST export” as a new display type.
  • There are many issues with REST in core; please consider contributing to RX (REST experience) tagged issues.

Decoupled Drupal 8

  • RELAXed Web Services (contrib) extends the core REST API to include revisions and file attachments, as well as cross-environment UUIDs.
  • RELAXed's mission aligns with movement in content staging and offline-first applications, and it uses the CouchDB API specification.

Setting up RESTful Drupal

  • Set up a Drupal 8 site through Acquia Dev Desktop. It's a free download and is the most painless way to spin up a Drupal site.
  • Check to make sure it's working locally: http://greatwideopen.dd:8083, for example.
  • Drush is the fastest way to quickly download and install dependencies.

Installing required modules

Now that you have a working Drupal site, you'll need to set up some dependencies. From your Drupal site, let's enable a few core modules that deal with Web Services.

							$ drush en -y hal basic_auth serialization rest
						

Installing required modules

You can either use rest.settings.yml to configure Drupal's core REST server, or you can download the REST UI module by Juampy NR to hit the ground running.

							$ drush dl restui
$ drush en -y restui
						

Installing required modules

Go to the REST UI configuration page and enable HTTP methods on content entities (and views, if you so desire).

Installing required modules

Now let's very quickly generate some content so we can quickly get content out of Drupal. This will generate 20 nodes with filler text.

							$ drush dl devel
$ drush en -y devel
$ drush en -y devel_generate
$ drush genc 20
						

Testing APIs with Postman

Postman is an excellent tool to quickly test your REST API and to make sure that your data is provisioned correctly for REST clients. It's available as a Chrome extension or a desktop app.

Testing APIs with Postman

Let's perform a GET request against Drupal's REST API for node/1. This fetches a node, and it will only work without authentication if you enable GET for anonymous users.

							GET /node/1?_format=json HTTP/1.1
Host: dcnj2016.dd:8083
Accept: application/json
Cache-Control: no-cache
Postman-Token: 6c55fb8b-3587-2f36-1bee-2141179d1c9c
						

Testing APIs with Postman

Let's add a node to Drupal by making a POST request against /entity/node.

							POST /entity/node HTTP/1.1
Host: dcnj2016.dd:8083
Accept: application/json
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 7776d489-e9bb-cad2-d289-24aa76f8f8a6

{
  "type": [
    {"target_id": "article"}
  ],
  "title": [
    {"value": "Lorem ipsum dolor sit amet adipiscing"}
  ],
  "body": [
    {"value": "This is a totally new article"}
  ]
}
						

Testing APIs with Postman

PATCH will update our node with new content.

							PATCH /node/23 HTTP/1.1
Host: dcnj2016.dd:8083
Accept: application/json
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: c1e4df7e-b17b-2256-75c8-55629c8329c7

{
  "nid": [
    {"value": "23"}
  ],
  "type": [
    {"target_id": "article"}
  ],
  "title": [
    {"value": "UPDATE UPDATE UPDATE UPDATE"}
  ],
  "body": [
    {"value": "Awesome update happened here"}
  ]
}
						

What is React?

  • The beginnings of React
  • React is a library, not an MVC framework
  • Differences with other frameworks
  • Drawbacks of React

The beginnings of React

  • Started in 2013, React has quickly grown in popularity due to its declarative approach to state, colocation of templates with view logic, and unopinionatedness about the stack.
  • In the last few years, a larger ecosystem has grown around React.

React is a library, not an MVC framework

  • React is “a library for building composable user interfaces” with reusable components.
  • Many people think of React as the V (view) in MVC (model–view–controller).
  • Because React is a library, there are many starter kits that you can use, since there is no consensus around a canonically acceptable set of libraries for routing, etc.

Differences with other frameworks

  • Traditionally, MVC frameworks using a declarative approach and data binding needed to apply changes directly on the DOM.
  • Angular 1, for example, manually updated DOM nodes.
  • React uses a Virtual DOM, which is an abstract DOM that allows different UI states to be diffed. In this way the most efficient DOM manipulation is possible.

Drawbacks of React

  • React is still in 0.x and as such is inherently unstable, with many BC-breaking changes in minors.
  • React builds are challenging due to the lack of coverage of the stack. Flux architectures and the Redux library are some tools that figure commonly in React builds, along with libraries such as react-router.
  • React has much less focus on Web Components support than Angular 2 and Ember.

React architecture and components

  • React dependencies
  • Express server setup
  • Routing
  • React components
  • Request handling
  • Client-side rendering

React dependencies

You don't have to use npm with React, but it's highly recommended (these examples use npm). If you want to use JSX, React has an additional dependency on Babel. Let's initialize a project and create package.json, our dependency list. npm can do this for us.

							$ npm init -y
$ npm install --save react react-dom
						

React dependencies

We'll be using ES2015, so you'll need to add some development dependencies ...

							$ npm install --save-dev babelify babel-preset-es2015 babel-preset-react babel-cli

						

React dependencies

... and create a .babelrc file in your project root.

							// .babelrc
{
  "presets": ["es2015", "react"]
}

						

React dependencies

To test this locally and use server-side rendering, we'll use ejs, a server-side template engine, and Express, a Node.js framework. These are application (not build) dependencies.

							$ npm install --save ejs express

						

React dependencies

We'll also want to install react-router, which is the most popular routing solution for React.

							$ npm install --save react-router

						

React dependencies

Your dependencies in package.json should now look like this:

							{
// name, version, etc.
  "dependencies": {
    "ejs": "^2.4.1",
    "express": "^4.13.4",
    "react": "^0.14.7",
    "react-dom": "^0.14.7",
    "react-router": "^2.0.1",
  },
  "devDependencies": {
    "babel-cli": "^6.6.5",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "babelify": "^7.2.0",
    "webpack": "^1.12.14"
  }
}
						

Express server setup

Now let's set up our server. In your root, create a new server.js. This is adapted from Jack Franklin's excellent 24ways article on universal React.

							// server.js
import express from 'express';

const app = express();
app.use(express.static('public'));
app.set('view engine', 'ejs');
app.get('*', (req, res) => {
  res.render('index');
});

app.listen(3003, 'localhost', (err) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log('Listening on 3003');
});
						

Express server setup

In order for Express to recognize our HTML, we need to save it as views/index.ejs.

							// views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello world!</title></head>
  <body>Hello world!</body>
</html>
						

Express server setup

Now we can start the server locally on port 3003.

							$ ./node_modules/.bin/babel-node server.js
# If you have it installed globally:
$ babel-node server.js
						

Express server setup

To make this easier, you can alias npm start in package.json.

							// package.json
{ // "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "./node_modules/.bin/babel-node server.js"
  },
  // "keywords": [],
}
						

Express server setup

Since React and its router will be controlling how our markup changes, let's substitute our “Hello world” for a placeholder.

							// views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello world!</title></head>
  <body>
<%- markup %>
</body> </html>

Routing

Create a new file called routes.js, which will contain the routes we define.

							// routes.js
import AppComponent from './components/app';
import IndexComponent from './components/index';
import NodesComponent from './components/nodes';

const routes = {
  path: '',
  component: AppComponent,
  childRoutes: [
    {
      path: '/',
      component: IndexComponent
    },
    {
      path: '/nodes',
      component: NodesComponent
    }
  ]
};

export { routes };
						

Routing

Let's update our server.js file to provide server-side rendering and routes. Above our invocation of Express, insert the following:

							// server.js
import express from 'express'; // Already present
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import { routes } from './routes';

// const app = express();
						

React components

Now that we've defined routes, we need to match them to components. First we need our overarching AppComponent, within which all of the child components will reside (this.props.children).

							// components/app.js
import React from 'react';

export default class AppComponent extends React.Component {
  render() {
    return (
      

Decoupled Drupal with React

{ this.props.children }
); } }

React components

Now for our other two components. IndexComponent lies inside AppComponent ...

							// components/index.js
import React from 'react';

export default class IndexComponent extends React.Component {
  render() {
    return (
      

Welcome to the home page

); } }

React components

... as does NodesComponent.

							// components/nodes.js
import React from 'react';

export default class NodesComponent extends React.Component {
  render() {
    return (
      

This is the nodes page

); } }

React components

Now let's make sure our server render recognizes any components we want to render there.

							// server.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';
import { routes } from './routes'; // Everything up to here already present
import AppComponent from './components/app';
import IndexComponent from './components/index';
import NodesComponent from './components/nodes';

// const app = express();

						

Request handling

Move further down in server.js to provide request handling:

							// server.js
// app.set('view engine', 'ejs');
app.get('*', (req, res) => {
  match({ routes, location: req.url }, (err, redirectLocation, props) => {
    if (err) {
      res.status(500).send(err.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (props) {
      // props means we have a valid component to render.
      const markup = renderToString(<RouterContext {...props} />);
      // Render index.ejs, but interpolate our custom markup.
      res.render('index', { markup });
    } else {
      res.sendStatus(404);
    }
  });
});

// app.listen(3003, 'localhost', (err) => {})
						

Request handling

Let's add a quick navigation bar to the overarching AppComponent.

							// components/app.js
import React from 'react';
import { Link } from 'react-router';

export default class AppComponent extends React.Component {
  render() {
    return (
      

Decoupled Drupal with React

  • <Link to='/'>Home</Link>
  • <Link to='/nodes'>Nodes</Link>
{ this.props.children }
); } }

Client-side rendering

To provide client-side rendering, we will need to include a production-ready build.

							// views/index.ejs
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello world!</title></head>
  <body>
    
<%- markup %>
<script type="text/javascript" src="bundle.js"></script> </body> </html>

Client-side rendering

Now let's provide a client.js which will be part of our bundle. It'll give us React for the client side.

							// client.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from 'react-router';
import { routes } from './routes';
import createBrowserHistory from 'history/lib/createBrowserHistory';

ReactDOM.render(
  <Router routes={routes} history={createBrowserHistory()} />,
  document.getElementById('app')
);
						

Client-side rendering

Now we'll build our bundle.

							$ npm install --save-dev webpack babel-loader

// webpack.config.js
var path = require('path');
module.exports = {
  entry: path.join(process.cwd(), 'client.js'),
  output: {
    path: './public/',
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel'
      }
    ]
  }
};
						

Client-side rendering

Then execute Webpack to create bundle.js. The Webpack command will use our config script.

							$ ./node_modules/.bin/webpack
						

React and decoupled Drupal

  • Cross-origin requests
  • React and REST in Drupal
  • Working with Drupal data in React

Cross-origin requests

You will need to allow your React application to have access to your Drupal back end. For example, in Apache 2:

							Header set Access-Control-Allow-Origin "*"

						

Cross-origin requests

You can also use the CORS module, which provides a configuration UI at /admin/config/services/cors.

							$ drush dl cors
$ drush en -y cors
						

Cross-origin requests

You can set it like this to allow certain domains to access the origin. Don't forget to clear caches after modifying configuration.

							*|http://localhost:3003
						
							$ drush cr
						

React and REST in Drupal

To make requests against Drupal's REST API, I recommend using superagent, which has a lightweight API and is comparable to jQuery AJAX.

							$ npm install --save superagent
						

React and REST in Drupal

Let's go back to our original GET request using Postman.

							GET /node/1?_format=json HTTP/1.1
Host: dcnj2016.dd:8083
Accept: application/json
Cache-Control: no-cache

						

React and REST in Drupal

  • To make synchronous requests during the server-side render, you can make an HTTP request whose response is then used by renderToString so that it is part of React's rendered markup.
  • For both synchronous and asynchronous requests, you can use componentDidMount to dynamically insert data with the Virtual DOM.

Working with Drupal data in React

Let's go back to our NodesComponent.

							// components/nodes.js
import React from 'react';

export default class NodesComponent extends React.Component {
  render() {
    return (
      

This is the nodes page

); } }

Working with Drupal data in React

Let's make an asynchronous request with superagent.

							// components/nodes.js
import React from 'react';
import superagent from 'superagent';

export default class NodesComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: '',
      body: ''
    }
  }
  componentDidMount() {
    this.serverRequest = superagent
      .get('http://greatwideopen.dd:8083/node/1?_format=json')
      .set('Accept', 'application/json')
      .end(function (err, res) {
        if (res.status === 200 && res.body) {
          var node = res.body;
          this.setState({
            title: node.title[0].value,
            body: node.body[0].value
          });
        }
      }.bind(this));
  componentWillUnmount() {
    this.serverRequest.abort();
  }
  // render() {
}

						

Working with Drupal data in React

Then, in our render function:

							// components/nodes.js
  // componentWillUnmount() {
  //   this.serverRequest.abort();
  // }
  render() {
    return (
      

{this.state.title}

{this.state.body}
); } }

React and the Drupal ecosystem

  • Decoupled Drupal with React
  • Other Facebook projects
  • GraphQL and Drupal
  • Epilogue: React within Drupal

Decoupled Drupal with React

  • The new Lullabot.com redesign is a fully decoupled React application against a CouchDB replication of the Drupal back end.
  • In progressive decoupling, Drupal constructs the initial page structure and a JavaScript application or client-side framework “takes over” further rendering on the client side.
  • In Drupal-aware full decoupling, Drupal provides important information about Drupal's server-side render in JSON to a JavaScript app or client-side framework which uses it for a full page render (see RESTful Panels).

Progressively decoupled Drupal with React

  • React is an exceptional candidate because it is unopinionated about the stack and can be used, for example, with or without a Flux library like Redux. It can act solely as a rendering layer.
  • Miles Blackwood's drupal-react_blocks module uses React to approximate a realtime push-powered Recent Comments block.
  • Matt Davis' Decoupled Blocks module aims to accomplish progressive decoupling in a framework-agnostic manner.

Other Facebook projects

  • Relay is a framework that juxtaposes data fetching needs and React components in the same place; in other words, Relay connects view logic to queries in the back end.
  • GraphQL is a query language that intelligently consolidates RESTful queries into a unified response and returns data according to the same schema as the request query.
  • Thanks to Sebastian Siemssen, a GraphQL server for Drupal 8 is soon to be released.

GraphQL and Drupal

  • In GraphQL, client requests and server payloads adhere to a shared shape.
  • The server houses the schema. The client dictates what it wants the server to provide.
  • GraphQL resolves typical limitations of RESTful architectures: many endpoints, response bloat, many round trips, no backwards compatibility, and no introspection.

GraphQL and Drupal

Remember this superagent example earlier? What if we could query our API without assuming that the response will be what we want?

							// components/nodes.js, abridged
    // import ...

    export default class NodesComponent extends React.Component {
      // constructor(props) {}
      componentDidMount() {
        this.serverRequest = superagent
          .get('http://greatwideopen.dd:8083/node/1?_format=json')
          .set('Accept', 'application/json')
          .end(function (err, res) {
            if (res.status === 200 && res.body) {
              var node = res.body;
              this.setState({
                title: node.title[0].value,
                body: node.body[0].value
               });
            }
          }.bind(this));
      // componentWillUnmount() {}
      // render() {
    }
						

GraphQL and Drupal

With Relay and GraphQL, you can specify the query from the client, such that a node fetch could look something like this.

							// components/nodes.js, abridged
    // import ...

    export default class NodesComponent extends React.Component {
      statics: {
        queries: {
          article: function () {
            return graphql`
              {
                entity {
                  node(id: 1) {
                    title
                    body
                  }
                }
              }
            `;
          }
        }
      }
      // render() {
    }
						

GraphQL and Drupal

That request will give you a JSON response payload that mirrors the structure of the request.

							{
  "entity": {
    "node": {
      "title": "Hello world!",
      "body": "Lorem ipsum dolor sit amet"
    }
  }
}
						
Back to the Future?

GraphQL and Drupal

GraphQL allows you to specify fragments to differentiate between, for example, Drupal content types.

							{
  entity {
    node(id: 1) {
      title
      body
      ... on EntityNodeArticle {
        fieldAuthor
        fieldTags {
          name
        }
      }
    }
  }
}
						

GraphQL and Drupal

You can also alias fields based on the needs of your client-side application.

							{
  content:entity {
    node(id: 1) {
      title
      body
      ... on EntityNodeArticle {
        author:fieldAuthor
        tags:fieldTags {
          name
        }
      }
    }
  }
}
						

GraphQL and Drupal

You can also specify variables from the client side which can facilitate a different response.

							query getArticle($nid: Int) {
  node(id: $nid) {
    title
    ... on EntityNodeArticle {
      body
    }
  }
}
						

GraphQL and Drupal

Directives allow you to alter execution behavior and conditionally include fields.

							query getArticle($published: Boolean, $nid: Int) {
  node(id: $nid) {
    ... @include(if: $published) {
      title
      body
    }
  }
}
						

GraphQL and Drupal

To learn more about GraphQL in Drupal, check out the recent Acquia webinar An Introduction to GraphQL with Sebastian Siemssen and yours truly.

Epilogue: React within Drupal?

  • Should you use Drupal with React as a view library to power its administrative user interfaces?
  • Should you use Drupal with React as a means to allow front-end developers to create rich UIs in site themes?
  • Should Drupal adopt React as a view library?

Thank you!

prestonso@prestonsopreston.so