Decoupled Drupal, GraphQL and React

Decoupled Drupal is gaining increased interest.  Decoupling refers to using Javascript for the front end of the website and Drupal for the back end content.  Many of the tutorials leave out critical information and even quite recent tutorials are already out of date because the software is changing so quickly.  Developing a production website under these conditions is still risky but has intriguing possibilities.  Javascript avoids page reloads and promises increased speed and separation of the display from the data.  But building a display yourself loses things like search engine optimization and accessibility that are provided by the standard html supplied by Drupal.

Drupal 8 provides a better integrated REST API that is touted as suitable for decoupled applications.  Unfortunately, after lots of investigation, there are problems with this approach.  This is not a criticism but just a realization that a general purpose REST API does not fill all the needs of a decoupled approach.  The content of nodes can be accessed but for other entities only metadata is available.  In my investigation I could only get information about blocks but not the block content.  When I tried to write code to expose the block content I found the lazy builder was used.  When I tried to render the block to get the content it threw an error because it was rendered too early.  It is possible to create a view to make the content available to REST but this requires work for everything that is required for the front end.  It is possible to architect the website so that all necessary data is contained in nodes but this seems to undermine a lot of the functionality provided by Drupal.  The other problem is this approach usually requires several queries to obtain the data that is needed and a lot of unnecessary data too.  This leads to complexity and inefficiency.

GraphQL takes a different approach.  It uses a schema that can be traversed to access the desired data.  Drupal has a configuration schema that can be used for this purpose.  The GraphQL contributed project provides a translation between the Drupal schema and the schema used by GraphQL.  Using this approach, GraphQL can access the data required through just one query.

Drupal 8 backend

Create a new Drupal 8 website using Composer.

composer create-project drupal-composer/drupal-project:8.x-dev {my_site_name_dir} --stability dev --no-interaction

I assume installing and setting up Drupal and contributed projects do not have to be explained. For a new website, create some content using the devel_generate module of the Devel project.  If you have Drush installed the content can be generated using

drush genc 50 --types=article

Otherwise, this can be done through the user interface at Configuration -> Generate content.

The GraphQL Drupal contributed project has been undergoing rapid change and lacks documentation.  "Please note that our documentation is outdated and in dire need of rewriting."  There are two modules in this project, graphql and graphql_core.  A separate project, GraphQL Views, should be included although it will not be used here.  The first step is using Composer to install GraphQL and then enable graphql and graphql_core.  Once these are enabled then test using the path /graphql.  This returns an error that no query was found.  /graphql/explorer and /graphql/voyager provide interactive ways of exploring the data.  I have found several tutorials for the Explorer so I will not cover that here other than a way for testing that the backend is working.

Decoupled frontend

The front end is the other half of a decoupled application.  React and Apollo will be used in this article.  React provides state for the Javascript app and Apollo provides caching.  The Javascript build stack is constructed using Node.js.  I will assume that Node.js is already installed.

It is complicated to get all the software for React properly installed so I will use Create React App to set up the frontend.

npm install -g create-react-app

Once Create React App is installed, the React app can be setup. If Composer was used to install the backend then go to that directory, just above the web root to install the app.

create-react-app frontend
cd frontend
npm install apollo-boost react-apollo graphql-tag graphql --save

The last line installs Apollo into the app. Then start the development server. Either npm or yarn can be used although I would recommend sticking with one or the other. Since I have been using npm above I will continue with it.

npm start

This should automatically launch a web browser for the React app.  If it does not appear then manually start a web browser with the URL localhost:3000.

Decoupled connection

Now comes where we connect the React app to the Drupal back end. From the front end directory change to the src directory and edit the App.js file. Start by specifying the connection.

import ApolloClient from "apollo-boost";

const client = new ApolloClient({
  uri: "{backend url}/graphql"
});

where {backend url} is the URL for the Drupal backend website. The app should recompile when the file is saved and then verify that no errors appear in the browser. Next is the GraphQL query.  I found remarkably little information to take a query built in Explorer and place it in the app.  Place the following code so the import is below the other imports and the rest is below the declaration of the client connection.

import gql from "graphql-tag";
...
client
  .query({
    query: gql`
      {
        nodeQuery {
          entities {
            ... on NodeArticle {
              nid
              title
              body {
                value
              }
            }
          }
        }
      }
    `
  })
  .then(({data}) => console.log({ data }));

Open up a console window in the browser. A couple of errors will need to be fixed. The first is the cross-origin request blocked error. To fix this the Drupal backend will need a service.yml. If there is not already one in the same directory as the settings.php, copy default.service.yml to service.yml and edit it.  At the end of the file is the core.config which should be updated to appear as follows.

   # Configure Cross-Site HTTP requests (CORS).
   # Read https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
   # for more information about the topic in general.
   # Note: By default the configuration is disabled.
  cors.config:
    enabled: true
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: ['x-csrf-token','authorization','content-type','accept','origin','x-requested-with', 'access-control-allow-origin']
    # Specify allowed request methods, specify ['*'] to allow all possible ones.
    allowedMethods: ['*']
    # Configure requests allowed from specific origins.
    allowedOrigins: ['*']
    # Sets the Access-Control-Expose-Headers header.
    exposedHeaders: false
    # Sets the Access-Control-Max-Age header.
    maxAge: false
    # Sets the Access-Control-Allow-Credentials header.
    supportsCredentials: false

Clear the Drupal caches and this error should go away. The second error is an unexpected character in the JSON data. This is because we are accessing Drupal without authenticating as a user. In the Drupal website go to permissions for GraphQL and enable Execute arbitrary GraphQL requests for anonymous users. Both of these will need to be tightened up for a production website but are okay for development.  Once the errors have been fixed the data returned by the GraphQL query should show up in the console.

GraphQL query result

The ApolloProvider is the other half to ApolloClient and it should be placed high enough in the render so that all queries to GraphQL are children. Modify the App.js by adding the lines in bold containing ApolloProvider.

import { ApolloProvider, Query } from "react-apollo";
.
.
.
class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <div classname="App">
. 
. 
.
        </div>
      </ApolloProvider>
    );
  }
}

Again, save the file and check the browser for any errors.

The last part is to construct a listing of articles much like a view.  First start by defining what the teaser for the article should contain.  Create a new file ArticleTeaser.js.

import React from 'react';

const ArticleTeaser = ({ article }) => (
  <div>
    <h3>{article.title}</h3>
    <div>{article.body.value}</div>
  </div>
);

export default ArticleTeaser;

In this case the teaser consists of the article title as the header and then the article body. The final part is to use the Apollo Query component to get the data and then put it in the render tree.

import ArticleTeaser from './ArticleTeaser';
...
const ArticlesView = () => (
  <Query
    query={gql`
      {
        nodeQuery {
          entities {
            ...on NodeArticle {
              nid
              title
              body {
                value
              }
            }
          }
        }
      }
    `}
  >
    {({ loading, error, data }) => {
      if (loading) return <p>Loading...</p>;
      if (error) return `Error: ${error.message}`;

      //console.log(data);
      return (
        <ul>
          {data.nodeQuery.entities.map(article => <li key={article.nid}><ArticleTeaser article={article} /></li>)}
        </ul>
      )
    }}
  </Query>
);

And the render tree should be updated to look like the following.

class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <div className="App-body">
            <ArticlesView />
          </div>
        </div>
      </ApolloProvider>
    );
  }

}

The article listing should now appear. Just a little CSS is needed to fix the alignment. Edit App.css and add the following.

.App-body {
  text-align: left;
}

Decoupled Drupal conclusion

This should demonstrate how to set up a development environment with a front end using React connected to a Drupal back end using Apollo and GraphQL. There is a lot that is not covered, such as ES6, JSX, Babel, and Webpack, but this is enough to get started. There also is authentication and production build stacks but already there is enough complexity covered in this article without tackling additional topics. The emphasis in this article is getting something working set up so that additional topics can be tackled. Decoupled Drupal is still in its infancy and there are many approaches that can be found and the software is still rapidly changing. But the gains in efficiency and response over loading pages provide a big incentive make more extensive use of Javascript in web applications.

Categories