 

 [    ](https://www.facebook.com/sharer/sharer.php?u=https://www.skvare.com/markdownify/node/424&title=Embedding%20a%20React%20Application%20in%20Drupal%20with%20TypeScript%2C%20CSS%20Modules%20%26%20Dev%20Server "Share to Facebook") [    ](https://www.linkedin.com/sharing/share-offsite/?url=https://www.skvare.com/markdownify/node/424 "Share to Linkedin") [    ](mailto:?subject=Embedding%20a%20React%20Application%20in%20Drupal%20with%20TypeScript%2C%20CSS%20Modules%20%26%20Dev%20Server&body=https://www.skvare.com/markdownify/node/424 "Share to Email") 

 

 

#  Embedding a React Application in Drupal with TypeScript, CSS Modules &amp; 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:

```plaintext
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`:

```yaml
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`:

```yaml
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:

```yaml
# 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
<?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`:

```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
<?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** `<strong>my_react_module.routing.yml</strong>`**:**

```yaml
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
<?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
<?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:**

```shell
mkdir react_app
cd react_app
npm init -y

```

**Install necessary dependencies:**

```shell
# 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** `<strong>package.json</strong>`**:**

```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`):

```typescript
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 (**`<strong>react_app/src/App.tsx</strong>`**):**

```typescript
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 (**`<strong>react_app/src/types/global.d.ts</strong>`**):**

```typescript
interface DrupalSettings {
  myReactModule?: {
    apiData?: any;
  };
}

declare global {
  interface Window {
    drupalSettings: DrupalSettings;
  }
}

export {};

```

**Access Drupal settings in your React components:**

```typescript
// 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`):

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

```typescript
// 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 (**`<strong>src/components/MyComponent.tsx</strong>`**):**

```typescript
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 (**`<strong>src/variables.scss</strong>`**):**

```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 (**`<strong>src/components/MyComponent.module.scss</strong>`**):**

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

```shell
npm run generate-types

```

This creates `.d.ts` files for each `.module.scss` file, enabling TypeScript support.

**Using the component in App.tsx:**

```typescript
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`):

```javascript
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:
    
    ```shell
    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:
    
    ```shell
    cd react_app
    npm run start
    
    ```
    
    **Important**: Uncomment the development configuration in `my_react_module.libraries.yml` and comment out the production configuration:
    
    ```yaml
    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:
    
    ```shell
    cd react_app
    npm run build
    
    ```
    
    **Important**: Comment out the development configuration and uncomment the production configuration in `my_react_module.libraries.yml`:
    
    ```yaml
    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:**

```typescript
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:

```typescript
// 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:

```typescript
// 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

```typescript
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](https://www.drupal.org/project/civicrm_entity) 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)](https://tanstack.com/query/latest) 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](/blog/simplifying-user-registration-webform-registration-handler) on simplifying user registration.