Skip to main content

Embedding a React Application in Drupal with TypeScript, CSS Modules & Dev Server

Embedding a React Application in Drupal with TypeScript, CSS Modules & Dev Server

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.

  1. Generate CSS Module Types: Before starting development, generate TypeScript declarations for CSS modules:

    cd react_app
    npm run generate-types
    

    This runs typed-scss-modules in watch mode, automatically generating .d.ts files whenever you create or modify .module.scss files. The watch mode means it keeps running and updates types in real-time as you work.

  2. Development: Use webpack dev server for hot reloading:

    cd react_app
    npm run start
    

    Important: Uncomment the development configuration in my_react_module.libraries.yml and 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/drupal
    

    The dev server provides hot reloading - your browser will automatically refresh when you make changes to React components.

  3. Production: Build optimized assets:

    cd react_app
    npm run build
    

    Important: 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:

  1. Enable REST module (core module)
  2. Create a View with REST Export display
  3. Configure the REST Export:
    • Path: /api/products
    • Format: JSON
    • Authentication: As needed

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.