This article demonstrates how to integrate a React application into a Drupal module, using practical implementation patterns with TypeScript, CSS Modules, and webpack dev server for development.
Overview
Embedding React in Drupal involves creating a custom module that serves as a bridge between Drupal's backend services and a standalone React application. This approach allows you to leverage React's modern frontend capabilities while maintaining integration with Drupal's content management and user systems.
Module Structure
A typical Drupal module with embedded React follows this structure:
my_react_module/
├── react_app/ # React application directory
│ ├── src/ # React source files
│ ├── dist/ # Built React assets
│ ├── package.json # Node.js dependencies
│ ├── webpack.config.js # Build configuration
│ └── tsconfig.json # TypeScript configuration
├── src/ # Drupal PHP classes
│ ├── Controller/ # Page controllers
│ ├── Plugin/Block/ # Block plugins
├── templates/ # Twig templates
├── my_react_module.info.yml # Module definition
├── my_react_module.module # Module hooks
└── my_react_module.libraries.yml # Asset libraries
Key Implementation Steps
1. Create the Module Definition
Define your module in my_react_module.info.yml:
name: 'My React Module'
type: module
description: 'Provides React-powered functionality'
package: 'Custom'
core_version_requirement: ^11
2. Set Up Asset Libraries
Configure your React assets in my_react_module.libraries.yml:
react_app:
js:
react_app/dist/main.js: { preprocess: false, minified: true }
css:
component:
react_app/dist/main.css: { preprocess: false, minified: true }
dependencies:
- core/drupal
For development, you can use an external webpack dev server:
# Development configuration (commented out in production)
# react_app:
# js:
# http://localhost:3000/main.js:
# type: external
# minified: false
# preprocess: false
3. Create a Theme Hook
Register a theme template in your module file:
<?php
/**
* Implements hook_theme().
*/
function my_react_module_theme($existing, $type, $theme, $path) {
return [
'my_react_module_app' => [
'variables' => [
'data' => NULL,
],
],
];
}
4. Create the Twig Template
Create templates/my-react-module-app.html.twig:
{{ attach_library('my_react_module/react_app') }}
<div id="react-root"></div>
The key element is the container div where React will mount.
5. Build Page Controllers or Blocks
Create a controller that renders your React app:
<?php
namespace Drupal\my_react_module\Controller;
use Drupal\Core\Controller\ControllerBase;
class ReactPageController extends ControllerBase {
public function renderPage(): array {
return [
'#theme' => 'my_react_module_app',
'#data' => $this->getData(),
'#attached' => [
'drupalSettings' => [
'myReactModule' => [
'apiData' => $this->getData(),
],
],
],
];
}
private function getData(): array {
// Return data that React needs
return [];
}
}
Add routing in my_react_module.routing.yml:
my_react_module.react_page:
path: '/react-app'
defaults:
_controller: '\Drupal\my_react_module\Controller\ReactPageController::renderPage'
_title: 'React Application'
requirements:
_permission: 'access content'
Update the controller to attach the library:
<?php
namespace Drupal\my_react_module\Controller;
use Drupal\Core\Controller\ControllerBase;
class ReactPageController extends ControllerBase {
public function renderPage(): array {
return [
'#theme' => 'my_react_module_app',
'#attached' => [
'library' => [
'my_react_module/react_app',
],
'drupalSettings' => [
'myReactModule' => [
'apiData' => $this->getData(),
],
],
],
];
}
private function getData(): array {
// Return data that React needs
return ['message' => 'Hello from Drupal!'];
}
}
Alternatively, create a block plugin:
<?php
namespace Drupal\my_react_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a React app block.
*/
#[Block(
id: "my_react_module_app",
admin_label: "React Application Block",
)]
class ReactAppBlock extends BlockBase {
public function build() {
return [
'#theme' => 'my_react_module_app',
'#attached' => [
'library' => [
'my_react_module/react_app',
],
'drupalSettings' => [
'myReactModule' => [
'apiData' => $this->getData(),
],
],
],
];
}
private function getData(): array {
return ['message' => 'Hello from Drupal!'];
}
}
6. Initialize React Application
Create the React app directory and initialize npm:
mkdir react_app
cd react_app
npm init -y
Install necessary dependencies:
# Core React
npm install react react-dom
# Development dependencies
npm install --save-dev typescript @types/react @types/react-dom
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev ts-loader css-loader sass-loader sass
npm install --save-dev mini-css-extract-plugin terser-webpack-plugin compression-webpack-plugin
npm install --save-dev typed-scss-modules
Add scripts to package.json:
{
"scripts": {
"start": "webpack serve --open --mode development",
"build": "webpack --mode production",
"dev": "webpack --watch --mode development",
"generate-types": "typed-scss-modules src --watch"
}
}
7. Configure the React Application
Set up your React app's entry point (react_app/src/index.tsx):
import { createRoot } from 'react-dom/client';
import React from 'react';
import App from './App';
// Mount React to the container created by Drupal
const rootElement = document.getElementById('react-root');
if (!rootElement) {
throw new Error('React root element not found');
}
createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Note: React.StrictMode is a development tool that helps identify potential problems in your application by intentionally double-invoking functions and effects. It has no effect in production builds.
Create a simple App component (react_app/src/App.tsx):
import React from 'react';
const App: React.FC = () => {
return (
<div>
<h1>My React App in Drupal</h1>
<p>Hello from React!</p>
{/* Add your components here */}
</div>
);
};
export default App;
8. Access Drupal Data in React
Define the window interface (react_app/src/types/global.d.ts):
interface DrupalSettings {
myReactModule?: {
apiData?: any;
};
}
declare global {
interface Window {
drupalSettings: DrupalSettings;
}
}
export {};
Access Drupal settings in your React components:
// Access data passed from Drupal
const drupalSettings = window.drupalSettings;
const moduleData = drupalSettings?.myReactModule?.apiData || {};
const App: React.FC = () => {
// Use moduleData in your React app
return <div>Your React Application</div>;
};
9. TypeScript Configuration
Configure TypeScript (react_app/tsconfig.json):
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"baseUrl": "src",
"allowSyntheticDefaultImports": true,
"lib": ["dom", "dom.iterable", "esnext"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Path mapping benefits:
The paths configuration allows clean imports without relative paths:
// Instead of: import Button from '../../../components/Button'
import Button from 'components/Button';
// Instead of: import { formatDate } from '../../utils/helpers'
import { formatDate } from 'utils/helpers';
// Import SCSS variables
import 'variables';
Important: When adding paths to tsconfig.json, you must also add corresponding aliases to your webpack configuration (see the resolve.alias section in the webpack config below).
10. CSS Modules with SCSS
CSS Modules provide scoped styling to prevent conflicts with Drupal's existing styles. Create component-specific styles:
Component file (src/components/MyComponent.tsx):
import React from 'react';
import * as styles from 'components/MyComponent.module.scss';
const MyComponent: React.FC = () => {
return (
<div className={styles.container}>
<h1 className={styles.title}>My Component</h1>
<button className={styles.primaryButton}>Click me</button>
</div>
);
};
export default MyComponent;
Create SCSS variables file (src/variables.scss):
// Colors
$primary-color: #007bff;
$primary-hover: #0056b3;
$background-light: #f5f5f5;
$text-dark: #333;
// Spacing
$spacing-sm: 0.5rem;
$spacing-md: 1rem;
$spacing-lg: 1.5rem;
// Border radius
$border-radius: 4px;
$border-radius-lg: 8px;
SCSS Module file (src/components/MyComponent.module.scss):
@use 'variables' as *;
.container {
padding: $spacing-md;
background-color: $background-light;
border-radius: $border-radius-lg;
}
.title {
color: $text-dark;
font-size: $spacing-lg;
margin-bottom: $spacing-md;
}
.primaryButton {
background-color: $primary-color;
color: white;
border: none;
padding: $spacing-sm $spacing-md;
border-radius: $border-radius;
cursor: pointer;
&:hover {
background-color: $primary-hover;
}
}
Generate TypeScript declarations for CSS modules:
Before using CSS modules, run this command to generate type declarations:
npm run generate-types
This creates .d.ts files for each .module.scss file, enabling TypeScript support.
Using the component in App.tsx:
import React from 'react';
import MyComponent from 'components/MyComponent';
const App: React.FC = () => {
return (
<div>
<h1>My React App in Drupal</h1>
<MyComponent />
</div>
);
};
export default App;
11. Advanced Webpack Configuration
Configure webpack with TypeScript, CSS Modules, and dev server (react_app/webpack.config.js):
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = (env, argv) => ({
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/',
clean: true,
},
mode: argv.mode || 'development',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.module\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCase',
},
sourceMap: argv.mode === 'development',
},
},
{
loader: 'sass-loader',
options: {
sourceMap: argv.mode === 'development',
sassOptions: {
includePaths: [path.resolve(__dirname, 'src')],
},
},
},
],
},
{
test: /variables\.scss$/,
use: [
{
loader: 'style-loader',
options: { injectType: 'styleTag' },
},
{
loader: 'css-loader',
options: {
sourceMap: argv.mode === 'development',
importLoaders: 1,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: argv.mode === 'development',
sassOptions: {
includePaths: [path.resolve(__dirname, 'src')],
outputStyle: 'expanded',
},
},
},
],
},
{
test: /\.scss$/,
exclude: [/\.module\.scss$/, /variables\.scss$/],
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: argv.mode === 'development',
},
},
{
loader: 'sass-loader',
options: {
sourceMap: argv.mode === 'development',
sassOptions: {
includePaths: [path.resolve(__dirname, 'src')],
},
},
},
],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
utils: path.resolve(__dirname, 'src/utils'),
components: path.resolve(__dirname, 'src/components'),
service: path.resolve(__dirname, 'src/service'),
context: path.resolve(__dirname, 'src/context'),
variables: path.resolve(__dirname, 'src/variables.scss'),
},
},
optimization: {
minimize: argv.mode === 'production',
minimizer: [new TerserPlugin()],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
...(argv.mode === 'development'
? [new webpack.HotModuleReplacementPlugin()]
: [
new CompressionPlugin({
test: /\.(js|css)$/,
algorithm: 'gzip',
threshold: 10240, // Only compress files larger than 10KB
minRatio: 0.8, // Only compress if compression ratio is better than 0.8
}),
]),
],
devServer: {
static: path.join(__dirname, 'dist'),
compress: false,
port: 3000,
hot: true,
allowedHosts: ['localhost.mysite.com'],
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},
},
devtool: argv.mode === 'development' ? 'source-map' : false,
});
Development Workflow
Prerequisites: Use the latest stable Node.js version for best compatibility and performance.
Generate CSS Module Types: Before starting development, generate TypeScript declarations for CSS modules:
cd react_app npm run generate-typesThis runs
typed-scss-modulesin watch mode, automatically generating.d.tsfiles whenever you create or modify.module.scssfiles. The watch mode means it keeps running and updates types in real-time as you work.Development: Use webpack dev server for hot reloading:
cd react_app npm run startImportant: Uncomment the development configuration in
my_react_module.libraries.ymland comment out the production configuration:react_app: js: http://localhost:3000/main.js: type: external minified: false preprocess: false css: component: http://localhost:3000/main.css: type: external minified: false preprocess: false dependencies: - core/drupalThe dev server provides hot reloading - your browser will automatically refresh when you make changes to React components.
Production: Build optimized assets:
cd react_app npm run buildImportant: Comment out the development configuration and uncomment the production configuration in
my_react_module.libraries.yml:react_app: js: react_app/dist/main.js: { preprocess: false, minified: true } css: component: react_app/dist/main.css: { preprocess: false, minified: true } dependencies: - core/drupal
Note: Keep the generate-types command running in a separate terminal during development for automatic CSS module type generation.
TypeScript Benefits
Using TypeScript in your React-Drupal integration provides:
- Type Safety: Catch errors at compile time, especially when handling Drupal data
- Better IDE Support: Auto-completion and refactoring capabilities
- API Interface Definition: Define types for Drupal API responses
- Component Props Validation: Ensure correct prop types across components
Example TypeScript interface for Drupal data:
interface DrupalSettings {
myReactModule: {
apiData: {
userId?: string;
permissions: string[];
apiEndpoint: string;
};
};
}
declare global {
interface Window {
drupalSettings: DrupalSettings;
}
}
CSS Modules Advantages
CSS Modules solve common styling challenges in Drupal:
- Scoped Styles: Prevent conflicts with Drupal's existing CSS
- Predictable Class Names: Generated class names avoid collisions
- Component Isolation: Each component has its own stylesheet
- Build-time Processing: SCSS variables and mixins are processed during webpack build
The exportLocalsConvention: 'camelCase' option allows you to use camelCase in JavaScript while keeping kebab-case in CSS.
Data Integration: From drupalSettings to REST APIs
Initial Data with drupalSettings
You can pass initial data from Drupal to React using drupalSettings. This is not necessary, but it can make the first app load faster by avoiding an additional HTTP request for the initial data. For dynamic data updates, use REST APIs - though you can still pass the first page of dynamic data through PHP if desired.
Dynamic Data with Drupal REST APIs
Setting up Drupal REST endpoint:
- Enable REST module (core module)
- Create a View with REST Export display
- Configure the REST Export:
- Path:
/api/products - Format: JSON
- Authentication: As needed
- Path:
TypeScript Data Structure
Define your API response interfaces:
// src/types/api.ts
export interface Product {
id: string;
title: string;
price: number;
category: string;
}
export interface ApiResponse<T> {
rows: T[];
pager: {
current_page: number;
total_items: number;
total_pages: number;
};
}
Fetching Data with fetch()
Simple service function:
// src/services/productService.ts
import { Product, ApiResponse } from 'types/api';
export const fetchProducts = async (): Promise<Product[]> => {
const response = await fetch('/api/products');
const data = await response.json();
return data.rows; // Drupal Views returns { rows: [...] }
};
Using in React Components
import { fetchProducts } from 'service/productService';
useEffect(() => {
fetchProducts().then(setProducts);
}, []);
Note: useEffect is not entirely needed for fetching data - you can fetch data in event handlers or other places, but we use it here for demonstration of loading data when the component mounts.
CiviCRM Integration
To integrate CiviCRM data with your React app, use the CiviCRM Entity module which makes CiviCRM data available as Drupal entities. You can then create normal Drupal Views that expose CiviCRM data through REST endpoints.
Advanced Data Fetching
For more complex data management with caching, background refetching, and optimistic updates, consider using React Query (TanStack Query) which provides powerful data synchronization for React applications.
Best Practices
- Separation of Concerns: Keep React code separate from Drupal PHP code
- Data Flow: Pass initial data via
drupalSettings, use AJAX/API for dynamic data - Asset Management: Use Drupal's library system for proper asset loading and caching
- Routing: Handle routing within React when appropriate, or use Drupal routing for page-level navigation
- Security: Validate and sanitize data passed between Drupal and React
- Performance: Implement code splitting and lazy loading for larger React applications
- CSS Isolation: Use CSS Modules to prevent style conflicts with Drupal themes
- TypeScript: Leverage static typing to catch integration issues early
Conclusion
This approach provides a clean separation between Drupal's backend capabilities and React's frontend power. The React application remains portable while benefiting from Drupal's content management, user authentication, and API services. The key is using Drupal's library system and drupalSettings to create a bridge between the two systems.
For a high level look at how these handler plugins behave inside Drupal, take a look at our site builder friendly article on simplifying user registration.