Lots of people in the Drupal community are eager to learn React these days, following Dries's announcement that React is coming to Drupal.
At NEDCamp in 2018 I presented on how to dip your toe into embedding a react application into a Drupal framework (video on drupal.tv).
This is the long-delayed blog post to follow up to the presentation.
Our approach was fundamentally this:
- we wanted to possibly embed multiple React apps on the site eventually, so we wanted to treat our base React libraries as common across the site.
- we needed to marry React routing and Drupal routing so that we could occupy a whole "namespace" of the site
- we wanted Drupal to store all the entities managed by the front-end, so we had to settle on storage and an API
React Libraries
We wrote a small react_libraries module to expose the libraries for React that we thought we would use everywhere and wanted consistent on every site.
Besides the .info.yml file for the module, the only other thing in the module is the libraries' definitions.
# react_libraries.libraries.yml react: js: https://unpkg.com/react@16/umd/react.production.min.js: external: true https://unpkg.com/react-dom@16/umd/react-dom.production.min.js: external: true react-dev: js: https://unpkg.com/react@16/umd/react.development.js: external: true https://unpkg.com/react-dom@16/umd/react-dom.development.js: external: true
All this does is suck in the libraries from CDN, including a prod (react) version and a dev (react-dev) version.
Our smaller apps just depend on this module and then attach the react_libraries/react(-dev) libraries as needed (you'll see that next).
One lesson learned is that we started with create-react-app, so to make this work we had to eject from that and remove the libraries that are normally in the build app bundle. Next time, we will build our app up from scratch rather than using the scaffolding.
Routing
The way React apps work is that they handle the routing by changing the URL with JavaScript and allow the app to deal with what to do given that route. But, as the page isn't refreshing, it's all just one path to Drupal. The problem comes in when a user bookmarks a URL and expects it to work (and it should). To handle this scenario, we assumed a 'namespace' in the routing by declaring /my-react-app/*
to belong to our React app. In Drupal 7, this would've "just worked," as any path registered auto-assumes that anything appearing on the URL after that are just arguments to the route. In Drupal 8, this is no longer true, so we have to sort of fake that old behavior.
To do that, we need a custom module. As part of that module, we can define routing--and we tell our route that there is a single argument passed ({react_route}), and we set the default value of that parameter to "" if it is not passed at all (i.e., you navigate to /my-react-app by itself).
# my_react_app.routing.yml my_react_app.overview: path: /my-react-app/{react_route} defaults: _controller: \Drupal\my_react_app\Controller\MyReactAppController::overview _title: 'My React App' react_route: ''
But, alas - this does not match the path /my-react-app/pathpart1/pathpart2
- so this is not complete yet.
Next, we need to create an Inbound Path Processor, by dropping a new class in our module's src/PathProcessor/ folder.
namespace Drupal\my_react_app\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
class MyReactAppPathProcessor implements InboundPathProcessorInterface {
public function processInbound($path, Request $request) {
if (strpos($path, '/my-react-app/') === 0) {
$names = preg_replace('|^\/my-react-app\/|', '', $path);
$names = str_replace('/',':', $names);
return "/savant-tools/$names";
}
return $path;
}
}
What the above code does is strip out the "namespace" part of our route, and then replace all the forward-slashes with colons so that it appears as a single route parameter. Essentially, PHP is just throwing this out anyway, since it's the front-end that will be using this route information in JavaScript.
The Controller
The final missing piece is - well, what DOES Drupal actually serve up for markup when we hit anything at my-react-app/*? That's defined by the Controller that your routing.yml file refers to. Your routing class gets dropped in your module's src/Controller folder.
<?php
namespace Drupal\savant_tools\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Controller for My React App.
*/
class MyReactAppController extends ControllerBase {
/**
* Renders the react app.
* @return array
* The render array.
*/
public function overview() {
$build = [];
// @TODO - do dev / prod here. (/react or /react-dev)
// Ideally, make this configurable somehow.
$build['#attached']['library'][] = 'react_libraries/react-dev';
// This is where you attach the additional library from your
// module that contains the non-React-libraries code
$build['#attached']['library'][] = 'my_react_app/actualapp';
// Finally, drop your main mount point for React.
// This ID can be whatever you use in your app.
$build['#markup'] = '<div id="root"></div>';
return $build;
}
}
Fin!
At this point, you're now free to write that whole slick frontend!
One last thing to mention, another alternative to this is to mount a React application onto a node. Using JD Flynn's module React Mount Node you can simply specify a node, the div ID, and the library you've registered with your React App in it. You will need React fully bundled, or you'll need to attach your react_libraries on every page or through some other mechanism, and the routing isn't handled with as much elegance - but if you have simpler needs, this is a great way to go!