Isomorphic (universal) JavaScript app using ReactJS + Drupal 8 + Webpack

This project is based in the project of “Example of integration with React and Webpack for universal (isomorphic) React rendering, using Limenius/ReactBundle and Limenius/LiformBundle” but using Drupal 8 instead of Symfony 3 for the REST API. You have a LIVE demo here.

Summarizing, the idea is create a decoupled (AKA Headless) Drupal 8 sandbox with server-side and client-side React rendering (AKA Isomorphic) with Webpack. I modified the original sandbox made by the guys of Limenius (I deleted some parts for now, for example the Redux part) and the goal is comparing the mandatory parts in Symfony 3 project and in Drupal 8 project. You can find both projects here:

* Isomorphic React App With Symfony 3
* Isomorphic React App With Drupal 8

All the code explained below is in the project’s files mentioned above. I’ve divided this tutorial in 4 parts: REST API, React app, Integrating the React app in Drupal 8 and Final result.


1. REST API

In Symfony, we will use a third party library called Doctrine (I won’t extend to explain this because this explanation would belong to Symfony’s part) to represent the Entity called Players.

With the proper controller (I’ll explain later about the controllers/routing) we have the REST Endpoint.

In Drupal, we will use Content Types (I installed Serial Field module because I needed an autoincrement field, of course this is not mandatory in any case !). First, we will create the Content Type we want to represent (called Players) with these fields:

After this we will add some content of this Content Type we’ve already created. Then we will use a new View to export the data but before that, we must install the WebServices modules to be able to export the REST Endpoint:

Now it’s the time to create the View:

And the configuration should be something like this:

Editing the view (Show/Settings). Note: If there’s any problem displaying the field_id (Serial Field), we can try to check the checkbox RAW OUTPUT for this field.

The contextual filter matches all the urls /rest/getPlayers/Content:id to create the endpoint only for the element with that id. I configured in this way: when the filter is NULL (there’s no value in the path) it fetchs ALL the results for the field ID.

The REST Endpoint is the same than in Symfony:

2. React app

The React app is based in the Example of integration with React and Webpack (Webpack Encore) for universal (isomorphic) React rendering, using Limenius/ReactBundle and Limenius/LiformBundle but simplified. This is the structure of the project:

I will explain very quickly the main files (more info in the GitHub’s page project of the guys of Limenius):

  • webpack.config.js → Webpack configuration for client-side.
  • webpack.config.serverside.js → Webpack configuration for server-side.
  • node_modules → Node packages.
  • client → JavaScript and CSS code.
  • app/Resources/webpack-server →Server side JavaScript bundle.
  • web/webpack-client → Client side JavaScript bundle.

Basically with Webpack we will generate 2 files (web/webpack-client/client-bundle.js and app/Resources/webpack-server/server-bundle.js and the CSS for the client side, web/webpack-client/stylesheets/client-bundle.css), for the client-side and server-side rendering.

For the client-side rendering we will attach the JS and CSS files in the main html template (we will see in the next chapter where exactly) and for the server-side rendering we will use ReactBundle to render it. If you want to test the React app (without data of course, and only client-side rendering), you can create an index.html file in the root of the folder. It would be something like this:

<!doctype html>
<html lang=”en”>
<head>
<meta charset=”utf-8">
<meta name=”viewport” content=”width=device-width, initial-scale=1">
<title>React App</title>
</head>
<body>
<div id=”root”></div>
</body>
<link href=”app/Resources/webpack-client/stylesheets/client-bundle.css” rel=”stylesheet”>
<script src=”app/Resources/webpack-client/client-bundle.js”></script>
</html>

And add these lines to the file client/js/clientEntryPoint.js:

import React from ‘react’;
import ReactDOM from ‘react-dom’;
ReactDOM.render(<ExampleApp baseUrl=’’/>, document.getElementById(‘root’));

To finish with this chapter, I will explain how the React app works. The purpose of an isomorphic app is do double rendering: the first request made by the web browser is processed by the server while subsequent requests are processed by the client. In this project(s) to make this possible, it uses Twig (both Drupal 8 and Symfony use it) where at certain moment in a Twig template file we will render the React component using this:

{{ react_component(‘ExampleApp’, {‘props’: props, ‘rendering’: ‘both’}) }}

This Twig function react_component,(which uses PhpExecJs) provided by ReactBundle, will render the React component ExampleApp in server-side and client-side modes depending the value of the parameter rendering, in a <div> that will serve as container of the component.

  • both’, does the server-side and client-side rendering.
  • server_side’, does only the server-side rendering
  • client_side’, does only the client-side rendering.

So (extracted from the writting of the guys of Limenius, thanks again), inside of it, the bundle will place all the HTML code that results of evaluating your component. It will do so by calling PhpExecJs, using your server-bundle, generated by Webpack as context, and retrieving the outcome.

When your client-side JavaScript runs, React will find this </div>tag and will recognize it as the result of rendering a component. It won't render it again (unless the evaluation of your client-side code differs), but it will take control of it, and, depending on the actions performed by the user, it will re-render the component dynamically.

Depending the kind of rendering, the React component will use clientEntryPoint.js or serverRegistration.js which will make to use ExampleAppClient.js or ExampleAppServer.js to startup the app. Why two different files ? Because the routing is slightly different depending if the render is in client-side or server-side (we will use React Router package to handle the routing). In those files, we can see how React Router works ,and there begins the app.

3. Integrating the React app in Drupal 8

Now we will integrate our React app in Symfony and Drupal 8.

Install theme

I installed Drupal 8 Zymphonies Theme, but we can use whatever. We could attach the client CSS & JS files in the theme (templates/layout/html.html.twig) like I did in the main template file in Symfony (base.html.twig) but I prefer to attach it in the module we will use to handle the routes (see next section Routing). So in Symfony:

In Drupal 8, in the template file of the theme (templates/layout/page.html.twig) I deleted all the regions, I just only kept the one to display the content and a Bootstrap navbar (this navbar is only for demonstration, to prove the difference with this navbar and navbar in a spa).

Location of the React App & Routing

In Drupal 8 the simplest way to provide routes is defining them in the routing YAML file in a module (In Symfony we can define the routes in a controller in src/AppBundle/Controller). So, the first thing will be create a module (in this example, called spa). Before continuing with the setup of the module I will show the structure of the files in Symfony and in Drupal 8 with the React App inside:

In Symfony

In Drupal 8

Content of the module called ‘spa’ in Drupal 8:

  • spa.info.yml → Descriptive file.
  • spa.libraries.yml → Here it’s declared the libraries we will use in this module, in this case the Webpack bundle for client-side (JS and CSS) (as I said before, in Symfony is placed in base.html.twig).
  • spa.module → There’re 2 functions. It’s important to declare which variables we will pass from the controller to the Twig view. In our React module is important to pass the variable props.
  • spa.routing.yml → Defines the matching of routes, and the controllers of them. It’s important to point out the ‘path’ part which matches the route, and the ‘controller’ part, which last parameter is the function of the controller that will be executed when matching the route defined in ‘path’.

Now is the turn to talk about the templates (templates) and controllers (src/Controller) folders in Drupal 8:

  • The template(s) are placed in the module’s folder also, in the ‘templates’ subfolder. In this project, the template file is spa.html.twig which is exactly the same in the Symfony project react_example.html.twig. In this template file is placed the declaration of react_component (explained previously).

After that, we have to install/enable the package react-bundle. In both cases we will install the package with composer, executing this in the folder root of the project (Note: Since the version 0.13.x of react-renderer, there is some kind of problem and doesn’t let to show the ‘client_side’ rendering. The problem is in the file:

vendor/limenius/react- renderer/src/Limenius/ReactRenderer/Twig/ReactRenderExtension.php

so we will install the version react-bundle 0.12.0 which requires react-renderer version 0.12.1):

composer require limenius/react-bundle: “^0.12.0"

Then, to enable it, in Symfony we must add this line in the file App/AppKernel.php:

In Drupal 8 is a bit more complicated: We have to edit the file

vendor/twig/twig/lib/Twig/Environment.php

adding these lines:

use Limenius\ReactRenderer\Renderer\PhpExecJsReactRenderer;

use Limenius\ReactRenderer\Twig\ReactRenderExtension;

global $base_url;

// Change the route below to server side Webpack bundle file.

$renderer = new PhpExecJsReactRenderer($base_url.’/modules/spa/spa/app/Resources/webpack- server/server-bundle.js’);

$ext = new ReactRenderExtension($renderer, ‘both’);

$this->addExtension(new Twig_Extension_StringLoader());

$this->addExtension($ext);

The final result:

and

The route of the server-bundle.js in Symfony is located in: app/config/config.yml

  • The controller(s) are placed in the subfolder ‘templates’ inside the module’s folder. In the ‘spa’ controller we have almost the same functions than in the controller DefaultController in Symfony. The function insertPlayer doesn’t have any kind of validation or inject prevention, this is due my limited time so it’s something we must have in mind to do. Let’s compare these functions:

Symfony

Drupal 8

After all, we can enable the module for Drupal 8 and setting the ‘front’ page (and control this path in the spa.routing.yml as we saw before in the setup of the module):

And also ‘Disable’ the cache because if in the server-side rendering we have the Dynamic Page Cache enabled, we won’t see the changes done in the page until we clear the cache.

4. Final result

Drupal 8

Symfony (I’ve populated the database with the same data like in Drupal 8)

The result is exactly the same (except some margin in Symfony project’s one, I didn’t pay much attention to the style of the projects). Now we can try to add players or compare the difference using the normal navbar and the spa’s navbar:

Normal navbar

Spa’s navbar

And that’s all for now, ideas and suggestions are welcome in the comments below, thank you.

Like what you read? Give Jordi Sanchez a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.