This is a walkthrough of how to build a React component inside a Drupal theme, using Vite to build it and a Drupal library to load it.

Prerequisites

  • A Drupal 10 or 11 site with a custom theme (we’ll call it mytheme)
  • Node.js 18 or newer, which also installs npm
  • A Mac or Linux OS. You’d have to adapt these instructions a bit on Windows.

Check your Node and npm versions:

node --version
npm --version

Create the React app

In your terminal, go to the root of your custom theme (e.g. themes/custom/mytheme/) and create a React app in a subdirectory:

npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install

Vite is the build tool. It transpiles React’s JSX into plain JavaScript that browsers can run, and bundles everything into a few files.

Open my-react-app/src/App.jsx and replace it with:

export default function App() {
  return <div>Moo!</div>
}

The Vite template’s src/main.jsx already mounts App into an element with the id root, so we don’t need to do that ourselves.

Configure the build output

By default Vite adds a hash to its filenames (index-a1b2c3.js), which is great for caching, but awkward when Drupal needs to know the filename in advance. Replace my-react-app/vite.config.js with:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    emptyOutDir: true,
    copyPublicDir: false,
    rollupOptions: {
      input: 'src/main.jsx',
      output: {
        entryFileNames: 'app.js',
        assetFileNames: 'app.[ext]',
      },
    },
  },
})

This builds from src/main.jsx straight to dist/app.js and dist/app.css.

The template’s main.jsx also imports a global index.css that styles body and #root. In a Drupal theme you don’t want that leaking into the rest of your site, so delete the import './index.css' line from main.jsx, and delete the file.

Now build the app:

npm run build

You should see my-react-app/dist/app.js appear. You never edit that file directly. It’s just the output of the build process, based on the code in src/.

Load it in Drupal

Drupal loads JavaScript and CSS through libraries. In mytheme.libraries.yml, define one for the React app:

react-app:
  js:
    my-react-app/dist/app.js:
      attributes:
        type: module
      preprocess: false
  css:
    theme:
      my-react-app/dist/app.css: {}

Vite outputs an ES module, so the script needs type: module. Setting preprocess: false stops Drupal’s JS aggregation from trying to bundle it with other scripts.

Next, attach the library and add the target element in whichever Twig template you want the app to appear in, such as a page or block template:

{{ attach_library('mytheme/react-app') }}
<div id="root"></div>

Clear Drupal’s caches so it picks up the new library (drush cr, or Configuration > Development > Performance in the admin UI).

Test it out

This is our Hello World moment. At Entra we prefer to Moo!

Visit a page that uses that template. The Moo! from the React app should show up inside the #root div.

Set up Sass

Vite understands Sass out of the box once the preprocessor is installed:

npm install --save-dev sass

Create a src/style.scss file next to App.jsx:

$bg: #ccc;

.app {
  background-color: $bg;
}

Then update App.jsx to import it:

import './style.scss'

export default function App() {
  return <div className="app">Moo!</div>
}

Run npm run build again, refresh your page, and you should see the update.

Development workflow

It’s tiring to run npm run build every time you make a change. Vite can watch your files and rebuild on its own, so add a script to package.json:

"scripts": {
  "dev": "vite",
  "build": "vite build",
  "watch": "vite build --watch",
  "preview": "vite preview"
},

Then run it:

npm run watch

Now whenever you save a file, Vite rebuilds dist/app.js and dist/app.css. Reload the page and the changes should show up.

Next steps

This gets you a basic setup for building React with Sass in a Drupal theme.

One shortcoming is that this is a full production build on every save, and you have to reload the page by hand. Hot module reloading would fix both: your changes would show up in the browser instantly, without a reload. It’s a lot more fun to work with. It can be done by loading the script from Vite’s dev server (npm run dev) while you’re developing, but it takes a bit more wiring on the Drupal side.

If I find the time and inspiration, I’ll cover this in another blog post!