Drupal and GraphQL with React and Apollo

Building an app that displays a list of articles in Drupal in the traditional way is a straightforward task - meaning that we use Drupal for everything: backend configuration and data storage as well as frontend (usually twig in Drupal 8).

However, doing the same thing in a decoupled configuration, where we use for example Drupal for backend and data storage, and React as frontend, is not that easy.

To help you with this, this blog post aims to show how you can successfully build that.

Drupal and GraphQL

There are 3 things you'll need to build this: Drupal, React and something that can bind those two together, meaning that it can fetch the data from Drupal and make it available in React. For this third thing, we will use GraphQL and Apollo.

Drupal Setup

As mentioned, the first thing you'll need to do is to install Drupal. Nothing special to mention here, just do a plain Drupal 8 installation. Additionally, you should also download the GraphQL module or clone it from https://github.com/fubhy/graphql-drupal. For now, we will just install the GraphQL Content and the GraphQL Views modules (they will automatically install the GraphQL and GraphQL Core dependencies).

We want to list some articles, so just go ahead and create a few (or use the Devel generate module if you want). Be sure to also create a simple view that lists articles (don't worry about displays right now, just list the title of them).

So now you have the Article content type (which was already there) and the Articles view which you just created, and you want to expose them via GraphQL. For this, you need a schema. You don't have to write it yourself, you just have to expose your configuration.

To expose content, just go to /admin/config/graphql/content and there you should see a list with all your entity types and bundles that you can expose. We want to expose the Article content type, so just click on Content and then Article.

Graphql Expose Content

Then you have the possibility to choose which fields to expose. And you do that by selecting a View mode. So before actually exposing the Article bundle, we need to configure a view mode. I recommend creating a new one, let's call it graphql, so that you can easily see it is used to expose data to GraphQL.

Go to admin/structure/display-modes/view and create the new view mode. Now you can go back to admin/config/graphql/content, click again on Content and Article and you should be able to select GraphQL from the view modes list. Don't forget to click on the Save configuration button.

Next you can go to the Manage display tab of the Article content type (/admin/structure/types/manage/article/display), and configure the GraphQL display.

Let's say we want to expose the body field(the title is automatically exposed).

Graphql View Mode
 

The second thing we want to expose is the Articles view. To do that, just go to edit your view and add a GraphQL display. Let's change the machine name to graphql_articles. Rebuild the cache and that's it. You just exposed your view to the GrapqQL schema.

Now, how can you be sure that what you did has really changed the schema. There is a very easy way to check this. There is a tool called GraphiQL which you can use to run GraphQL queries and also check the schema. For this, just install the GraphQL Explorer module, and then navigate to /graphql/explorer on your site. On the top right you should have a link called Docs which will expand the Documentation Explorer on click.

In the search box, just input Article and you should see two entries: NodeArticle (this is the article content type you exposed) and articlesGraphqlArticlesView which is your view. If you see those, then you properly configured the schema.


Graphql Explorer

As mentioned, with GraphiQL you can run GraphQL queries. It's time to see if we get any data.

So for the article view, just use this query:

{
  articlesGraphqlArticlesView {
    ...on NodeArticle {
      entityId
      title
      body
    }
  }
}

Put this in the left panel, click the run button and you should see the results in the right panel. The above query can be translated like this: give me the results from the articlesGraphqlArticlesView view, and when the type of the object is NodeArticle, give me the title, the body and the entityId fields.

With the query above we just tested both things we exposed: the Articles view and NodeArticle content type.


React

The second thing to do is to prepare your frontend. In this example, we'll use React. Probably the easiest is to use create-react-app. So just go ahead and create a new app and make sure you can start it and have the home screen displaying properly.

Let's build now an ArticlesView component which can display a list of ArticleTeaser components.

Here is the ArticlesView component:

import React from 'react';
import ArticleTeaser from '../ArticleTeaser';

const ArticlesView = ({articles}) => (
  <ul>
    {articles.map(article => <li key={article.id}><ArticleTeaser article={article} /></li>)}
  </ul>
);

export default ArticlesView;


And here is the ArticleTeaser component:
 

import React from 'react';

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

export default ArticleTeaser;

 

Finally, the App.js can look something like this for now (with a dummy list of articles):


import React, { Component } from 'react';
import ArticlesView from './ArticlesView';

const articles = [
  {
    id: 1,
    title: 'First article',
    body: 'This is the first article',
  },
  {
    id: 2,
    title: 'Second article',
    body: 'This is the second article',
  }
]

const App = () => (
  <div className="App">
    <ArticlesView articles={articles} />
  </div>
);

export default App;

If you run this you should see a list of two articles with their title and body. This is pretty much the pure frontend you have to do. The rest is just supplying the real data to the frontend. This needs some additional packages to be installed. So here we go!
 

GraphQL and React Apollo

To connect to our GraphQL server from React, we will use the React Apollo library. To install it just run:

yarn add react-apollo

The first thing we will do is to update our ArticlesView component. We want to get the list of the articles injected into the component. So we will use the graphql Higher Order Component provided by the react-apollo library and run a basic query to get the results. 

Here is the updated code for the ArticlesView component:

import React from 'react';
import { gql, graphql } from 'react-apollo';
import ArticleTeaser from '../ArticleTeaser';

const query = gql`
  query articlesQuery {
     articlesGraphqlArticlesView {
      ...on NodeArticle {
        id:entityId
        title
        body
      }
    }
  }
`;

const withQuery = graphql(query, {
  props: ({ data: { loading, articlesGraphqlArticlesView } }) => ({
    loading,
    articles: articlesGraphqlArticlesView
  }),
});

const ArticlesView = ({ loading, articles }) => {
  if (loading) {
    return null;
  }
  return (
    <ul>
      {articles.map(article => <li key={article.id}><ArticleTeaser article={article} /></li>)}
    </ul>
  )
};

export default withQuery(ArticlesView);

The first thing to do is to build the GraphQL query that we'll use to fetch our data. We can do this using the gql function. Then we wrap our initial ArticlesView component with the Higher Order Component returned by grapqhl. The GraphQL HOC will get our query as a parameter, as well as a config object where we can specify among other things the props that our ArticlesView component will get.

In our case, the ArticlesView component will receive a loading flag which can be used to check if the data is still loading from the server and the articles which are basically the articlesGraphqlArticlesView result from the GraphQL request.

We also need to update a bit the App component, because we need to wrap everything into an ApolloProvider component.

Here is the code:

import React, { Component } from 'react';
import ArticlesView from './ArticlesView';
import { ApolloClient, createNetworkInterface } from 'apollo-client';
import { ApolloProvider } from 'react-apollo';

const client = new ApolloClient({
  networkInterface: createNetworkInterface({
    uri: 'http://url_to_drupal_site/graphql'
  }),
});

const App = () => (
  <ApolloProvider client={client}>
    <ArticlesView />
  </ApolloProvider>
);

export default App;

As you can see, the App component wraps the ArticlesView component inside ApolloProvider (which is initialized with an ApolloClient) and this means that any component bellow ApolloProvider can use the HOC returned by graphql to make requests to the GraphQL server using the ApolloClient which we instantiate in the client parameter.

This is a very important thing to keep in mind. If you move the ArticlesView component outside of the ApolloProvider, your app will not work anymore.

At this point, you can try to run your app, but you may receive a few js errors.

The first may be this one:

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This means that your backend server does not allow sending content to a different domain than its own. So we'd need to enable the cors.config in your services.yml file (in sites/default). More about that here: https://www.drupal.org/node/2715637

Here's a possible configuration:

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

Rebuild the drupal cache and this error should dissapear.

A second error you could get is a 403 for the request to /graphql. The problem is that the anonymous user does not have access to run GraphQL queries. Just go to the administer permissions page and give the Execute arbitrary GraphQL requests permission to the anonymous role. Reload the page and you should see a list of articles (which of course you have to create first in Drupal).

This is the most simple listing app that you can do with Drupal, GraphQL, React and Apollo. However, there are many other features you may need. In the next section, we'll discuss pagination and filtering (including contextual arguments).

Filtering

For this example, we need to add a new field on our Article content type. Let's say we add a Year field on our articles, which is of type List (integer) and can have for now 3 possible values: 2017, 2016 and 2015.

It may look something like this:

Year field.

Then add the Year field as an exposed filter.

Year filter

And you can also expose the year field in the GraphQL display mode of the Article content type so that you can also fetch it for displaying in the frontend.

And now all you have to do in the frontend is to adjust the query and use the filter, like this:

const query = gql`
  query articlesQuery {
     articlesGraphqlArticlesView(filter: {field_year_value: "2015"}) {
      ...on NodeArticle {
        id:entityId
        title
        body
      }
    }
  }
`;

One important thing to keep in mind for now: if you have a filter on a view, you must provide a value for it in the query, otherwise the empty string will be used and most probably you will not get any results. And in this case, if you just want to print all the articles, use 'All' for the field_year_value filter.

Now, of course, having that year value hardcoded in the query is good enough for a demo, but in real apps you will probably want to have that variable. For this, we have to update our query to contain variables and our config object which is used by the graphql HOC to inject that variable into our query.

First, let's see the new query:

const query = gql`
  query articlesQuery($year: String!) {
     articlesGraphqlArticlesView(filter: {field_year_value: $year}) {
      ...on NodeArticle {
        id:entityId
        title
        body
        fieldYear
      }
    }
  }
`;

And second, let's see the new GraphQL call:

const withQuery = graphql(query, {
  options: () => ({
    variables: {
      year: 2016,
    },
  }),
  props: ({ data: { loading, articlesGraphqlArticlesView } }) => ({
    loading,
    articles: articlesGraphqlArticlesView
  }),
});

What we did so far was to just move the hard-coded value from the query into the config object. It's a step forward, but it is still kind of hard-coded. Ideally, we'd have the year specified as a prop on the ArticlesView component.

And in fact we can do that, because the options function can get the component props as a parameter, like this:

options: (props) => ({
  variables: {
    year: props.year,
  },
})


Which means you will use the ArticlesView component in App.js like this:

<ArticlesView year={2016} />


If you try now the app and replace the year with different values you should get different results.

Contextual filters are pretty much the same as filters. So, for example, if we want to add the author of the articles as a contextual filter to the view, to query that in fronted just add the contextual_filterI argument to the articlesGraphqlArticlesView field, like this:

articlesGraphqlArticlesView(filter: {field_year_value: $year}, contextual_filter: {uid:"2"}) {
  ...
}

And just like for the year filter, you can use a variable for the uid filter.


Pagination

To use pagination, first, you have to update the view itself. It is not important how many items per page you set, it is just important that the view has a page. The number of items per page will be specified in the query.

If you do that the articlesGraphqlArticlesView field will get two additional arguments: page and pageSize.

You will have something like this:

articlesGraphqlArticlesView(page: 0, pageSize: 2, filter: {field_year_value: $year}, contextual_filter: {uid:"2"}) {
  ...
}

And if you run now your app you will actually see that there are no results returned. The reason is that the articlesGraphqlArticlesView will now return a different structure. It will be an object with two attributes: count that represents the total number of results and results which are the results of the current page.

The new query therefore is (for simplicity, the filters and contextual filters are removed):

const query = gql`
  query articlesQuery {
     articlesGraphqlArticlesView(page: 0, pageSize: 2) {
      count
      results {
        ...on NodeArticle {
          id:entityId
          title
          body
          fieldYear
        }
      }
    }
  }
`;

Of course, now we have to update the props of the graphql config object.

props: ({ data: { loading, articlesGraphqlArticlesView } }) => ({
  loading,
  articles: articlesGraphqlArticlesView && articlesGraphqlArticlesView.results,
}),

If you reload the page, you should see now 2 articles. The last thing to do is to have a link or a button that when clicked it will load more entries.

Here is the LoadMore component:

import React from 'react';

const LoadMore = ({ loadMoreHandler }) => (
  <a onClick={(e) => loadMoreHandler(); e.preventDefault()} href="https://www.amazeelabs.com/en">Load more</a>
);

export default LoadMore;

And add the LoadMore component in the ArticlesView component:

<div>
  <ul>
    {articles.map(article => <li key={article.id}><ArticleTeaser article={article} /></li>)}
  </ul>
  <LoadMore />
</div>

 

Now we have to supply a loadMoreHandler to the LoadMore component. Luckily, when using the config object of the grapqhl we have access to a function called fetchMore() which we can use to rerun the query and fetch more (or other) results. Using that function, we will add a new prop which will be injected into our ArticlesView component, which will be the loadMoreHandler.

Here is the updated query (which now contains the page and pageSize as variables) and the updated graphql call:

const query = gql`
  query articlesQuery($page: Int!, $pageSize: Int!, $year: String!) {
     articlesGraphqlArticlesView(page: $page, pageSize: $pageSize, filter: {field_year_value: $year}) {
      count
      results {
        ...on NodeArticle {
          id:entityId
          title
          body
          fieldYear
        }
      }
    }
  }
`;

const withQuery = graphql(query, {
  options: (props) => ({
    variables: {
      year: props.year,
      page: 0,
      pageSize: 2,
    },
  }),
  props: ({ data: { loading, articlesGraphqlArticlesView, fetchMore } }) => ({
    loading,
    articles: articlesGraphqlArticlesView && articlesGraphqlArticlesView.results,
    total: articlesGraphqlArticlesView && articlesGraphqlArticlesView.count,
    loadMoreEntries() {
      return fetchMore({
        variables: {
          // The page number will start with 0, so the next page is basically
          // the number of current results divided by the page size.
          page:
            articlesGraphqlArticlesView.results &&
            Math.ceil(
              articlesGraphqlArticlesView.results.length / 2,
            ),
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          // If there are no new results, then just return the previous result.
          if (!fetchMoreResult.articlesGraphqlArticlesView.results) {
            return previousResult;
          }
          return {
            articlesGraphqlArticlesView: {
              ...previousResult.articlesGraphqlArticlesView,
              count: fetchMoreResult.articlesGraphqlArticlesView.count,
              results: [
                ...previousResult.articlesGraphqlArticlesView.results,
                ...fetchMoreResult.articlesGraphqlArticlesView.results,
              ],
            },
          };
        },
      });
    },
  }),
});

So fetchMore will rerun the query. The config object it gets as a parameter has two main properties: the variables which will be used to merge new variables into the existing ones (in our case we just want to get the next page) and updateQuery, which is actually a function that can access the previous result as well as the new result and has to return the new articlesGraphqlArticlesView field.  

As seen above, we use that to merge the new results into the existing ones. And it's now time to use the new props that we inject into our ArticlesView component.

Here is the updated component:

const ArticlesView = ({ loading, articles, total, loadMoreEntries }) => {
  if (loading) {
    return null;
  }
  return (
    <div>
      <ul>
        {articles.map(article => <li key={article.id}><ArticleTeaser article={article} /></li>)}
      </ul>
      { total > articles.length ? <LoadMore loadMoreHandler={loadMoreEntries}/> : null}
    </div>
  )
};

export default withQuery(ArticlesView);

And that's all I wrote, folks! You now have a view built in Drupal that you expose in GraphQL and use Apollo and React to display it in frontend, with pagination and filters.

August 2, 2017
3 Comments

Get our Newsletter






Comments

It's very cool to see how this setup would be put together. I would love to hear more on the reasons to use Apollo and GraphQL. My first question is whether React could instead use Drupal's REST capabilities to get the data. And if so, my next question is what benefits Apollo and GraphQL provide. (I imagine they simplify the building of the UI, but it would be nice to see that info in the article.) Thank you for posting!

By Brock Fanning
2 weeks ago

Hi Brock!

Here are my answers to your questions:
1. Could React use Drupal's REST capabilities to get the data? Absolutely! React does not impose a specific way of getting the data. As long as you can feed your components with data, React will work. In this particular example, you can see how at the top of the blog post I pass a dummy articles array which contains some test data. Now, instead of hardcoding that data you could make a request to an endpoint which can return a json, you parse it, and that's it.
2. What are the benefits of Apollo and GraphQL? I will point out the benefits of GraphQL, since Apollo is just a tool that queries the server using the GraphQL syntax. The main benefits are:
- allows the frontend to ask only for the data which it really needs. No more, no less. This translates into traffic optimization, which is always good for mobile.
- you can have more frontends which use the same backend and each one could ask for different data. Now, if you want to refactor one of the frontends, if you would use REST, it could be that you may also have to change the structure of the json output (the happy case is when you only need to add new fields) so it has a side effect on the other frontends. With GraphQL, you do the changes only in that particular frontend.
- can save you from doing more roundtrip requests. For example: if you want to display the articles of an user (with more details), with REST you usually ask first for the user profile from where you can access a list of articles, then you do a request to get the data for each article. Of course, you could optimize this by having a specialized endpoint that returns that specific list, with those specific fields. But then what if you have to display the same list but with other fields in some other part of your app. And if you have multiple frontends... you will start to create a very complex and complicated REST server. One that can be used ONLY for those frontends.

And the list could continue, but in my opinion the top 2 benefits of GraphQL over REST are:
- you truly decouple the frontend and the backend. The frontend is the one who decides what kind of data (and how much) it wants, not the backend. Which makes frontend refactoring much easier.
- the amount of data sent over the network (and the number of requests) can be reduced a lot.

I also suggest to checkout this video: https://www.howtographql.com/basics/1-graphql-is-the-better-rest/

By Vasi
2 weeks ago

Thanks for this super helpful tutorial! Haven't gotten a chance to test out headless Drupal yet but when I do, this is the model I would like to use. Cheers!

By Dave Rothfarb
1 week ago

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
icon
What is Amazee Labs?