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.
Much of Drupal's robustness is lost.
Source: GraphQL with Nick Schrock, React London meetup (10/15)
http://greatwideopen.dd:8083
, for example.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
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
Go to the REST UI configuration page and enable HTTP methods on content entities (and views, if you so desire).
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
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.
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
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"}
]
}
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"}
]
}
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
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
... and create a .babelrc
file in your project root.
// .babelrc
{
"presets": ["es2015", "react"]
}
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
We'll also want to install react-router, which is the most popular routing solution for React.
$ npm install --save react-router
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"
}
}
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');
});
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>
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
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": [],
}
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>
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 };
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();
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 }
);
}
}
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
);
}
}
... as does NodesComponent
.
// components/nodes.js
import React from 'react';
export default class NodesComponent extends React.Component {
render() {
return (
This is the nodes page
);
}
}
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();
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) => {})
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 }
);
}
}
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>
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')
);
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'
}
]
}
};
Then execute Webpack to create bundle.js
. The Webpack command will use our config script.
$ ./node_modules/.bin/webpack
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 "*"
You can also use the CORS module, which provides a configuration UI at /admin/config/services/cors
.
$ drush dl cors
$ drush en -y cors
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
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
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
renderToString
so that it is part of React's rendered markup.componentDidMount
to dynamically insert data with the Virtual DOM.
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
);
}
}
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() {
}
Then, in our render
function:
// components/nodes.js
// componentWillUnmount() {
// this.serverRequest.abort();
// }
render() {
return (
{this.state.title}
{this.state.body}
);
}
}
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() {
}
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() {
}
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"
}
}
}
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
}
}
}
}
}
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
}
}
}
}
}
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
}
}
}
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
}
}
}
To learn more about GraphQL in Drupal, check out the recent Acquia webinar An Introduction to GraphQL with Sebastian Siemssen and yours truly.