React & Drupal - A simple yet powerful example project

July 6, 2018

Submitted by twfahey on Tue, 06/19/2018 - 16:26

React. It's talked about at nearly every Drupal event these days, in the context of headless and progressively decoupled implementations. There are lots of examples and articles out there that talk about integrating Drupal and React. I think having articles like these out on the open web is important, as each provides a perspective that could potentially make the "A-ha!" moment happen, and that is valuable for the net and open source!

First, let's get our Drupal setup with a REST endpoint. I'll be using Drupal 8.6.x-dev branch in this version. We want to enable these modules:

  • `cors`
  • `rest`

I prefer to do this via drush:

tyler$ drush en cors rest -y

Now we have the appropriate modules for the React app integration. Let's setup the `cors` rules so that our React app won't have problems querying the site. Go to `/admin/config/services/cors`, and add the domain of our React app, in this case `localhost:3000`:

image

 

I should note that these are the equivalent of "full admin privileges" for the localhost:3000 domain. There is more to CORS that I want to cover in a future blog post. But for now, these rules will do nicely for purposes of local development. 

*UPDATE*: As an alternative to dealing with CORS locally, I realized it's possible to disable CORS in the services.yml. As seen in the development.services.yml, which is typically in sites/default in a D8 codebase and recommended to enable in your settings.local.php for local development. You can see the section related to CORS config is right there:

# 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: false
    # Specify allowed headers, like 'x-allowed-header'.
    allowedHeaders: []
    # 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

So, either way, you should be ready to properly handle requests from the React app.

Next, let's setup a View REST export for Articles that will be our source of data for the React app.

I think that rather than a screenshot here, I'll paste the exported config of the View: (Hooray for config management in D8!)

uuid: a7b6a42c-a1d4-4a2f-bbca-b58c36c82c9f
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
- node.type.article
module:
- node
- rest
- serialization
- text
- user
id: article_rest_api
label: 'Article REST API'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: {  }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: {  }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: serializer
row:
type: fields
options:
inline: {  }
separator: ''
hide_empty: false
default_field_elements: true
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: {  }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
body:
id: body
table: node__body
field: body
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: text_default
settings: {  }
group_column: value
group_columns: {  }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
changed:
id: changed
table: node_field_data
field: changed
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: medium
custom_date_format: ''
timezone: ''
group_column: value
group_columns: {  }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: changed
plugin_id: field
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
type:
id: type
table: node_field_data
field: type
value:
article: article
entity_type: node
entity_field: type
plugin_id: bundle
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
header: {  }
footer: {  }
empty: {  }
relationships: {  }
arguments: {  }
display_extenders: {  }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:field.storage.node.body'
rest_export_1:
display_plugin: rest_export
id: rest_export_1
display_title: 'REST export'
position: 1
display_options:
display_extenders: {  }
path: api/articles
pager:
type: some
options:
items_per_page: 10
offset: 0
style:
type: serializer
options:
formats:
json: json
row:
type: data_field
options:
field_options:
title:
alias: ''
raw_output: false
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:field.storage.node.body'

Well, there's a lot there. The TL;DR is that we're exposing the title, body, and changed fields from Article nodes in JSON at the URL `/api/articles`. I encourage interested parties to import the config and take a deeper look at the specific configuration involved.

Now, I'll create some Article content that we can load up in our React app. You could optionally use something like devel_generate to automate this process and create a lot of content quickly.

Now there is article content that is being exported via our REST View to a URL that is web accessible. Cool! That's all we need for now on the Drupal side of things. Now it's time to create our React app.

Let's start out by scaffolding a new React project, using create-react-app:

tyler$ npx create-react-app blog-post-react-app

This will create our app. For this example, I'm thinking we'll show a simple table of the article data.

First, let's add the excellent axios project to our app for taking care of doing REST calls:

tyler$ yarn add axios

Let's open up the project, and create a new component that will query our REST endpoint. We'll use axios to make the REST call to our Drupal site. The URL will be derived from a prop we'll add to this component in the next step when we add it to our page. Let's call it `ArticleList`. Here's how I did it:

import React, { Component } from 'react';
import axios from 'axios';
class ArticleList extends Component {
constructor(props) {
super(props);
// Setting up initial state
this.state = {
articles: [],
}
}
// calling the componentDidMount() method after a component is rendered for the first time
componentDidMount() {
axios.get(this.props.source).then(article =>{
this.setState({
articles: article.data,
});
});
}
render() {
var articleData = this.state.articles.map((item) => {
return "<h4>" + item.title + "</h4>"
+ "<p>" + item.body + "</p>";
});
return (
<div>
{ articleData }
</div>
);
}
}
export default ArticleList;

So now, the ArticleList will do a GET request on the source URL, and set the `articles` in the component state to the data. This data is then mapped out and rendered in the render method. 

Now, we can simply add this component in our main App.js file. Replace the boilerplate text that comes with the create-react-app scaffolding, and add an instance of the component, with a source prop pointing to the local Drupal site's View export path, which in my case was `blog-drupal8.local`:

import React, { Component } from 'react';
import './App.css';
import ArticleList from "./ArticleList";
class App extends Component {
render() {
return (
<div className="App">
<ArticleList source={"http://blog-drupal8.local/api/articles?format=json"}/>
</div>
);
}
}
export default App;

 

Now, we can fire up the app and make sure we're getting the expected result:

tyler$ npm run start

 

Our app should automatically pop up in a browser once compiled....wait for it.....and, sweet! Success!

image

Though not production ready by any stretch of the imagination, we have setup the framework here for all sorts of fun integrations with Drupal REST API and React!

 

Comments

Add new comment