You don't need to go fully 'headless' to use React for parts of your Drupal site. In this tutorial, we'll use the Drupal 8 JsonAPI module to communicate between a React component and the Drupal framework.
As a simple example, we'll build a 'Favorite' feature, which allows users to favorite (or bookmark) nodes. This type of feature is typically handled on Drupal sites with Flag module, but let's say (hypothetically...) that you have come down with Drupal-module-itis. You're sick of messing around with modules and trying to customize them to do exactly what you want while also dealing with their bugs and updates. You're going custom today.
Follow along with my Github repo.
Configure a custom Drupal module
First thing's first: data storage. How will we store which nodes a user has favorited? A nice and easy method is to add an entity reference field to the user entity. We can just hide this field on the 'Manage Form Display' and/or 'Manage Display' settings since we'll be creating a custom user interface for favoriting.
When you install the Favorite module in the repo, it will add the field 'field_favorites' for you to the user entity: see the config/install directory.
Next up: define the part of the page that React will replace. Typically, you will replace an HTML element defined by an id with a React component. If this is your first time hearing this, you should do some basic React tutorials. I started with one on Code Academy.
Look at favorite.module. I use template_preprocess_node to add a div with id 'favorite' to every node on full view mode. This will be the div that I will replace with a React component.
Note that I also attach a 'library' called 'favorite/favorite' to the render array. This means my 'favorite' module needs to define a library called 'favorite' in a 'favorite.libraries.yml' file. This library defines which JavaScript file to include on the page.
Integrating React in a Drupal module
The JavaScript file we're attaching with a Drupal library is 'favorite.bundle.js.' This is actually a generated file that includes React and other dependencies as well as our custom JavaScript all in one. The tool we're using to do this is Webpack. Take a look inside the js directory of favorite module.
The JavaScript dependencies are being managed via npm as defined in the package.json file. To develop locally, run `npm install` within the js directory, which will download packages into a node_modules subdirectory. Note that there is a .gitignore file to prevent node_modules from being added to the repo. Everything the live site will need will be included in the favorite.bundle.js file, and the other files are only needed to regenerate that file with changes via webpack.
Webpack configuration is in the webpack.config.js file. If you add more js files (typically you add a new file for every React component), add them to the 'entry' array. In order to run webpack to generate the bundled js file, you should install it globally on your machine `sudo npm install webpack -g`. Now you should be able to run `webpack` in the js directory and have it recreate the bundled js file. It also catches syntax errors and gives helpful feedback, so pay attention if the command fails.
There is also a config file .babelrc in the js directory. Babel is a JS compiler which will convert code written with ES2015 syntax to be ES5 compatible. It also handles JSX, which is a syntax used to define React components. The webpack process will use Babel.
Creating the React component for Drupal
Finally, we have the custom React code in favorite.js. If you edit this, remember to run `webpack` to update the bundled js file. I typically look at React files starting at the bottom.
ReactDOM.render(<Favorite />, document.getElementById('favorite'));
The last line is replacing the HTML element with id of 'favorite' with a 'Favorite' React component. The rest of the file defines that Favorite component.
The render function outputs a link which will say either 'Favorite' or 'Unfavorite.'
render() {
if (this.state.user_uid == "0") {
return null;
}
var linkClass = 'unfavorited';
var text = 'Favorite';
if (this.state.favorited) {
linkClass = 'favorited';
text = 'Unfavorite';
}
return (
<a href="#" className={linkClass} onClick={this.toggleFavorite}>{text}</a>
);
}
In order for this logic to work, we need to have the current user's uid stored in the state (this.state.user_uid) as well as whether or not the user has already favorited this node (this.state.favorited). We also need to have a function 'toggleFavorite' in the component to handle when a user clicks the favorite/unfavorite link to update the database and change state.favorited.
Let's look at toggleFavorite() first and then how that initial state is set.
toggleFavorite() {
var favorited = !this.state.favorited;
this.saveFavorite(favorited);
}
saveFavorite(favorited) {
var endpoint = '/jsonapi/user/user/' + this.state.user_uuid + '/relationships/field_favorites';
var method = 'POST';
if (!favorited) {
method = 'DELETE';
}
fetch(endpoint, {
method: method,
credentials: 'include',
headers: {
'Accept': 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json'
},
body: JSON.stringify({
"data": [
{"type": 'node--' + this.state.node_type, "id": this.state.node_uuid}
]
})
}).then((response) => {
if (response.ok) {
response.json().then((data) => {
this.setState({
favorited: favorited
});
});
}
else {
console.log('error favoriting node');
}
});
}
The behavior depends on the current state.favorited. If the user had already favorited this node, we'll be unfavoriting it on click. Otherwise, we'll be favoriting it. To favorite it, we'll POST data to the JsonAPI endpoint, and to unfavorite it, we'll delete the data. The JsonAPI Drupal module handles relationships including entity references according to the JsonAPI Spec so there are special endpoints for adding or removing data from a relationship.
We're using 'fetch' to make the request. By specifying `credentials: 'include'` we'll pass along the user's cookie. Provided that the user has access to add and delete field_favorites on their own user account, the request should succeed.
Note that there is some more state data that we need here. We need to have the node type in this.state.node_type and the node's uuid in this.state.node_uuid. After the request is made, we update state.favorited via this.setState(). Changing the state causes a re-render so you should see the text of the favorite link update after click.
Other considerations for using React with Drupal
Finally, how did we get all the values we rely on related to the current user and current node into the state? We could have done this a few different ways. One approach would be to use the drupalSettings system to pass data from Drupal to the DOM on initial page load. In this example though, I created a custom JSON endpoint using Drupal routing/controller to provide the data that I want and call it from the React component.
Take a look at the constructor() and getData() methods in the Favorite react component. I use 'fetch' again, this time doing a GET request on my custom path, and then setting the state with the data I get back. The custom path is defined in favorite.routing.yml to use the controller FavoriteController.php, which returns Json with the desired data.