On Github prestonso / decoupled-drupal-react
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)
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>
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 (
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 (
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>
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
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 (
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.