Decoupled (or headless) Drupal is currently one of the biggest topics in the Drupal community. A decoupled Drupal installation separates the actual content of a Drupal site from its presentation into two independent components. Opinions differ on this approach, but there is no question that this topic is worth looking into.
Using a Drupal 8 installation as an example, the following will show how Decoupled Drupal can be implemented with React as the frontend and GraphQL as the connector. In connection with this, advantages and disadvantages, opportunities and current limitations of the components used will be discussed.
Decoupled Drupal
As already mentioned, a decoupled Drupal installation separates the front and back ends. The backend (Drupal) "only" serves as storage for the data and content. The frontend uses this storage as it wishes and has complete sovereignty over how it deals with the data and its presentation.
One of the advantages of independence is that both the frontend and backend can act independently without having to take the other into consideration. This means that complete redesigns are possible without changing a single setting in the backend.
In addition, Drupal is a unique system that normally requires frontend developers to have a comprehensive understanding of the backend in order to work effectively and, above all, efficiently. By separating these areas, frontend developers can work without Drupal knowledge and use their natural skills without having to acquire further knowledge.
This can also make you independent of different devices. A backend can be used for mobile applications, web applications or classic websites at the same time - the frontend always has control over which data it displays and how.
As always, however, there is also a downside: many functionalities that Drupal otherwise provides as standard and free of charge have to be implemented by the user. This includes layout, templating, routing, authentication, security, performance, etc. The list does not end here. The advantage of a CMS like Drupal is that it provides all the functionalities just mentioned plus many other features at no great extra cost. This is not to say that Decoupled Drupal is not worthwhile, but that you should carefully consider whether Decoupled Drupal is the right choice for your vision of a Drupal application.
React and GraphQL
React will be the basis for the frontend of the later example application. Originally developed by Facebook, React has made a name for itself in recent years as a clear and efficient library for displaying components in the frontend.
One of the reasons for this is the absence of templates and a more modular structure based on components. This ensures that React can be implemented on a small scale in any project for testing purposes and then built upon later.
React also introduces JSX - a file format that allows classic JS and HTML to be mixed. HTML tags can thus be easily used in JS without cumbersome concatenation, making the writing of individual render components clearer and more intuitive. More about the advantages of React can be found here: http://blog.thefirehoseproject.com/posts/13-reasons-react-taking-web-development-industry-storm/
GraphQL is the link between the front and back end and an alternative to REST that is gaining momentum and attention.
Probably the biggest difference to REST is that the client itself defines the structure of the data it wants to receive and receives exactly this structure from the server, which prevents typical REST problems such as "over-fetching" or "under-fetching". An article with a more detailed overview can be found here: http://nordicapis.com/5-potential-benefits-integrating-graphql/
GraphQL is particularly useful in the context of Drupal 8 due to the Contrib module of the same name, which makes it much easier to get started and use GraphQL.
Preparation
Before starting a practical example with React and GraphQL, some preparations need to be made. The basis for the Drupal installation is the drupal-composer/drupal-project from github, which can be installed with the command
composer create-project drupal-composer/drupal-project:8.x-dev
some-dir --stability dev --no-interaction
can be installed. If Composer is not yet installed, you can find instructions for Linux here: https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx. Regardless of this, all later examples can also be carried out without this Composer project and with a "normal" Drupal installation.
The following modules should be installed and activated:
-
GraphQL
-
GraphQL Core
-
GraphQL Mutations
-
GraphQL Content
-
GraphQL Content Mutations
-
GraphQL Explorer
-
Optionally, the other GraphQL modules can be activated for various applications, e.g. to obtain and change views or blocks with GraphQL. A more detailed overview of the individual modules and functions can be found here: https://www.amazeelabs.com/blog/
The following command can be used to install modules with Composer:
composer require drupal/module_name
Modules are only downloaded with this command, but not activated!
Various settings can now be made for the GraphQL module under /admin/people/permissions. The settings here relate exclusively to obtaining and changing the content type article, which is used as an example in this blog article.
Authentication is another major topic that this blog article will not deal with. For this reason, all queries in GraphQL are made as an anonymous user, which requires the following authorizations:
Which fields of a content type are available to GraphQL and can be read out is defined via a view mode.
A new view mode "graphql" should be created under /admin/structure/types/manage/article/display. All fields contained in this view mode can be queried and changed.
Then, under /admin/config/graphql/content, you can specify what type of content GraphQL should be available for. For the example of an article, only "Content" and "Article" need to be checked. The "graphql" view mode can now be selected for "Article".
Atomic React
Atomic React(https://arc.js.org/) is used as the starter kit for the front end with React in this example. Atomic React provides a solid foundation for developing a classic web application with dependencies to packages that enable routing, testing, module bundling, hot reloading and much more. In addition, the structure is based on the Atomic Design Methodology, which harmonizes very well with the component-based React.
To install Atomic React, the following command can be executed at the folder level where the drupal (or web) directory is located:
git clone -b master https://github.com/diegohaz/arc.git frontend
"frontend" specifies the name of the folder in which the React components are created. In the "frontend" folder, you can now create a
npm install
can now be executed to install the existing dependencies. Two additional packages are also required for later:
npm install --save apollo-client react-apollo
Atomic Design
Atomic design is an interface design method in which an interface is divided into individual components. Individual components can be viewed as examples within /frontend/src-example/components. "Atoms" are the smallest components and consist of a single HTML tag or a third-party component.
"Molecules" are several "atoms" combined. "Organisms" are made up of several "atoms", "molecules" or other "organisms". "Pages" are the individual pages on which "organisms" are mainly used. "Templates" determine the basic structure of a "page" and "themes" provide configurable styling.
Each of the directories already provides a collection of sample components. To view the sample project in the browser, the command
npm run dev:example
can be used. The following should be visible at http://localhost:3000/: https://arc.js.org/
React Basics
If you have not yet dealt with React, you should at least take a closer look at the chapters up to and including "Handling Events" on this page https://reactjs.org/docs/hello-world.html to get a rough understanding of React. The following practical example in particular should then be easier to understand.
To start the actual application, the command:
npm run dev
is used. Currently, only "Hello World" is output.
React Templating
Before the page is filled with further content, a template should be created which will form the basis for this and other pages. Two further files are required for the next step. Firstly, a template and a new organism "header".
/components/templates/DefaultTemplate/index.js:
import React from 'react'
import PropTypes from 'prop-types'
const DefaultTemplate = ({ header, children }) => {
return (
{header}
{children}
)
};
DefaultTemplate.propTypes = {
header: PropTypes.node.isRequired,
children: PropTypes.any.isRequired,
};
export default DefaultTemplate
A "DefaultTemplate" component is defined here, which is passed "header" and "children" as parameters. The component returns three
"proptypes" are also defined under the component. "proptypes" are used to validate transferred elements and keep them consistent. For the "header" element, the "node" type is set as "isRequired", which means that the transferred element must be a React component that can be rendered. There is no specification of the type for the "children". It is only assumed that nothing is passed.
/components/organisms/Header/index.js:
import React from 'react'
const Header = (props) => {
return (
{props.title}
)
};
export default header
The "Header" component only returns a
/components/pages/HomePage/index.js:
import React from 'react'
import { DefaultTemplate, Header } from 'components'
const HomePage = () => {
return (
}>
Hello World
)
};
export default HomePage
In order to use the template and the new "Header" organism on the current home page, these two components must be imported.
The "DefaultTemplate" template can then receive the "Header" elements as "header" and the content (here only a
If the page is updated, the markup should look like this:
React Routing
A second "page" "LoginPage" is required for routing. The quickest way to achieve this is to copy the "HomePage" component. The "title" of the "Header" component is then changed to "LoginPage" in "LoginPage" and the content of the
/components/App.js:
import React from 'react'
import {Switch, Route} from 'react-router-dom'
import {HomePage, LoginPage} from 'components'
const App = () => {
return (
)
};
export default App
The routing is carried out in the App.js file. The previous content has been replaced with the above code for reasons of clarity. The return of the "App" component contains a switch that renders different "pages" with their corresponding templates for different routes.
The "exact" attribute ensures that the corresponding components are only rendered for exactly these routes. Otherwise, for example, "LoginPage" would also be rendered for the /login/user route.
If the page is reloaded, the corresponding component is now displayed for the specified path.
GraphQL Basics
As already explained in the introduction, one of the major differences between GraphQL and REST is that the client can decide for itself which fields and resources it wants to receive. The available resources are defined by so-called "schemas" and "types".
The advantage of using GraphQL in conjunction with Drupal is that the GraphQL module has already completed these preparations. "schemas" and "types" are available for the majority of Drupal data and can be used immediately.
GraphiQL
GraphiQL is an IDE within the browser that enables GraphQL queries to be written, tested and executed. In Drupal, GraphiQL can be found under /graphql/explorer.
GraphiQL supports auto-completion based on the existing "schemas" and "types" as well as a "Documentation Explorer" that makes them visible. As a first example, the goal is to write a simple query that returns all nodes of content type "Article" with their "Id" and "Label". The following query performs this query:
GraphQL:
query BasicArticleQuery {
nodeQuery {
entities {
... on NodeArticle {
entityId
entityLabel
}
}
}
}
"query" represents the type of request, while "BasicArticleQuery" is the name of this query. "nodeQuery" is a special type of query that makes it possible to query nodes. "entities" is a field of this query, which provides all basic fields of a node. Here it is only possible to enter fields such as "entityId" or "entityType" - i.e. fields that are available for every type of node.
"... on NodeArticle" is a condition that ensures that only nodes of the content type "Article" are queried and its specific fields such as "title" or "body" can be entered. Regardless of whether they are entered inside or outside the condition, any number of fields can be specified at this point.
After execution, you receive a list of all articles with the values for their "entityId" and "entityLabel" fields.
GraphQL can also be used to create or change elements using so-called "mutations". In the next example, the aim is to create an item with the title "GraphQL".
GraphQL:
mutation CreateArticle($article: NodeArticleInput!) {
createNodeArticle(input: $article) {
entity {
... on NodeArticle {
title
}
}
}
}
Query Variables:
{
"article": {
"title": "GraphQL"
}
}
To understand this mutation, it is advisable to use the "Documentation Explorer". If you enter "createNodeArticle" in the search and look at the corresponding schema, you will see that this type of mutation requires an "input" parameter of the type "NodeArticleInput!". The "!" means that this parameter is required and that this mutation cannot be carried out without it.
By clicking on "NodeArticleInput!" you get an overview of all fields of an article. The only field that is required is "title"; all other fields are optional. For this reason, a variable "article" with the field "title" and the value "GraphQL" was created above. This variable can now be passed to the "CreateArticle" mutation.
Although the mutation is already called "createNodeArticle", the condition "... on NodeArticle" must be used, as otherwise only the basic fields of a node can be entered. However, "title" is a field that is specific to articles and is only available through this condition.
As "GraphQL" is assigned as a value to the "title" field in the "article" variable, this mutation sets the "title" field to "GraphQL" when an article is created. Executing this mutation should create such an article and return the article just created with the field "title" and the value "GraphQL".
As an alternative to GraphiQL, it is also possible to use the REST client Insomnia(https://insomnia.rest/). Although it does not offer a "Documentation Explorer", it also offers auto-completion and, unlike GraphiQL, the option of saving queries and reusing them later.
GraphQL in React
The goal now is to execute the queries shown above from React, get the corresponding data from Drupal and display it. The following new or modified components are required for this:
/components/organisms/ArticleList/index.js:
import React from 'react';
import {gql, graphql} from 'react-apollo';
import {AddArticle} from 'components';
const ArticleList = ({data: {loading, error, nodeQuery}}) => {
if (loading) {
return Loading.
..;
}
if (error) {
return {error.message}
;
}
return (
{nodeQuery.entities.map(entity =>
- {entity.entityLabel}
)}
);
};
export const ArticleListQuery = gql`
query BasicArticleQuery{
nodeQuery {
entities {
... on NodeArticle {
entityId
entityLabel
}
}
}
};
export default graphql(ArticleListQuery)(ArticleList);
The "ArticleList" organism returns an
- of all articles on a specific page. The query is carried out using the "graphql" function. To do this, the query is first parsed in "ArticleListQuery" and the results of the query are mapped to individual
- tags in conjunction with "ArticleList".
The query was not yet executed the first time Article List was called. However, the Boolean "loading" is true and signals that no results are available yet. If there was an error, this is also displayed.
If the query is no longer loading and there was no error, it is assumed that a result is available that can be used. In addition to a
- with all the articles on a page, this organism also returns the "AddArticle" component.
/components/molecules/AddArticle/index.js:
import React from 'react'; import { gql, graphql } from 'react-apollo'; import {ArticleListQuery} from '../../organisms/ArticleList/index' const AddArticle = ({mutate}) => { const handleKeyUp = (evt) => { if (evt.keyCode === 13) { evt.persist(); mutate({ variables: { "article": { "title": evt.target.value } }; refetchQueries: [{query: ArticleListQuery}], }) .then(res => { evt.target.value = ''; }); } }; return ( ); }; const CreateArticleMutation = gql` mutation CreateArticle($article: NodeArticleInput!) { createNodeArticle(input: $article) { entity { ... on NodeArticle { title } } } }; export default graphql(CreateArticleMutation)(AddArticle);
The "AddArticle" molecule uses a text field to create an article with the name entered in the text field by pressing the Enter key.
The structure is similar to that of the "ArticleList" organism. The "AddArticle" component consists of two parts. It contains a simple text field as a return value, which has a callback to an event handler. The event handler checks whether the key pressed is the Enter key and adds several attributes to the "mutate-prop".
Firstly, the content of the text field "title" is assigned as the variable "article". Secondly, "refetchQueries" is used to specify a query that is to be executed again after the mutation.
If you enter a title in the text field and press the Enter key, the mutation creates an article with a title that corresponds to the string entered in the text field. The "ArticleListQuery" is then executed again and now also displays the article just created in the list of all articles.
/components/pages/HomePage/index.js:
import React from 'react' import ApolloClient, { createNetworkInterface } from 'apollo-client'; import { ApolloProvider } from 'react-apollo'; import { DefaultTemplate, Header, ArticleList } from 'components' const networkInterface = createNetworkInterface({ uri: 'http://decoupled-project.dev/graphql' }); const client = new ApolloClient({ networkInterface }); const HomePage = () => { return ( }> ) }; export default HomePage
To actually execute the GraphQL queries just created, a GraphQL client is required, which means that some adjustments have to be made to the HomePage component. Apollo is used as the GraphQL client for this example.
First, a "networkInterface" is created, which contains the uri to the current Drupal installation with the endpoint /graphql as the only attribute. This "networkInterface" can now be used to set up a new GraphQL client with Apollo. Although the "DefaultTemplate" is still used for the "HomePage" component, the content is replaced by the "ApolloProvider" component, to which the GraphQL client is passed as a parameter.
The "ApolloProvider" component receives the "ArticleList" component as content, which in turn contains the "AddArticle" component. As a result, all required components are now displayed. After reloading the start page, the finished result should look something like this:
Depending on the existing articles in the Drupal installation, their list will of course look different. If you now enter a title for a new article (e.g. "Article 4") in the text field and confirm with the Enter key, it should appear shortly afterwards in the list of articles:
Troubleshooting
If the error "Network error: Failed to fetch" is displayed, this is due to the missing header "Access-Control-Allow-Origin" which must be set for cross-origin requests. In this case, we recommend changing the default.services.yml in /sites/default/files. The corresponding cors.config settings can be found at the bottom, which should look like this:
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
If this does not solve the problem either, there is the Chrome extension "Allow-Control-Allow-Origin: *", which does nothing other than set the missing header.
The future of GraphQL and Drupal
Anyone who has read this article up to this point should have gotten a rough overview of the possibilities and advantages of a decoupled Drupal installation in combination with GraphQL and React.
Nevertheless, there are currently still some elementary use cases with GraphQL that cannot be implemented (at least not only with the GraphQL module). For example, it is not yet possible to change or delete existing entities.
Another problem is authentication. For this article, the anonymous user was given authorizations such as "Create Content" or "Execute arbitrary GraphQL requests". In most cases, these authorizations should not be available to the anonymous user on a published page.
There has long been a module for REST that enables authentication using Oauth 2. With GraphQL, this is currently only possible with great effort and a lot of custom code. Although there are initial approaches to authentication with a hybrid of REST and GraphQL, these are not yet very advanced either.
In conclusion, it can be said that GraphQL is already a good alternative to REST in special cases. Nevertheless, GraphQL is not yet exploiting its full potential, especially in conjunction with Drupal. However, if you look at the development of the past few months, you can be positive that more and more features, which previously required a lot of custom code, are gradually finding their way into Drupal.
Further links:
-
Decoupled Drupal - Discussion: https://www.lullabot.com/articles/should-you-decouple
-
Documentation of react: https://reactjs.org/docs/installation.html
-
Documentation of Apollo with react: http://dev.apollodata.com/react/
-
Documentation of GraphQL: https://graphql.org/learn/
-
Atomic Design continuation: https: //atomicdesign.bradfrost.com/chapter-2/
-
GraphQL and REST: http://blog.strapi.io/how-graphql-and-rest-api-can-work-together/