14+ years building on WordPress / Replies in under 5 hours
Page Builders & Theming 8 min read · Updated July 2026

Build a WordPress Theme with React JS: Complete Guide

Photo of Ajay Khandal
Ajay Khandal
WordPress Developer
Steps to Build a Stunning WordPress Theme with React JS
TL;DR

You can build a WordPress theme where the frontend is a React app rather than PHP templates. WordPress handles content via its REST API; React handles rendering. Setup: use Vite (`npm create vite@latest my-theme -- --template react`) — not the deprecated `create-react-app`. Configure `vite.config.js` to output to `assets/js/main.js` and `assets/css/main.css` in the theme directory with fixed filenames (no hashing) so `functions.php` can reference them reliably. In `functions.php`, use `wp_enqueue_script` for the built JS file, then `wp_localize_script` to pass `window.wpData = { apiUrl, nonce, siteUrl }` to React — never hardcode the domain in React components. In React, fetch posts with `fetch(window.wpData.apiUrl + 'posts?_fields=id,title,excerpt,link')` and render with `dangerouslySetInnerHTML` (safe for your own WP data, already sanitized server-side). Theme files required: `style.css` (theme header comment), `index.php` (outputs `<div id="root"></div>`), `header.php`, `footer.php`. SEO limitation: client-side React rendering means search engines see an empty div on initial load — use Next.js with the WP REST API for a fully SEO-compatible architecture.

You can build a WordPress theme where the frontend is a React app rather than PHP templates. WordPress handles content management and data via its REST API; React handles rendering. This is sometimes called a “hybrid” or “headless-lite” approach — WordPress runs as normal (admin, plugins, content editing), but the visitor-facing output comes from React components rather than theme PHP files.

This approach makes sense when you need interactive UI behaviour (filtering, live search, infinite scroll, single-page navigation) that would be awkward to build in PHP. For a standard blog or brochure site, a traditional PHP theme or Gutenberg block development is simpler and performs equally well. Note that React blocks within Gutenberg (building custom blocks for the editor) is a different pattern — this guide covers the headless-lite theme approach, not block development. If you want the classic PHP-first path, the WordPress theme from scratch guide covers that in full.

Prerequisites

  • Node.js v18+ (LTS): required for Vite and React tooling
  • WordPress development environment: Local by Flywheel, Lando, or Docker
  • Basic React familiarity: components, hooks (useState, useEffect), JSX

Project setup with Vite

create-react-app was retired by the React team in early 2023 and is no longer maintained. Use Vite — it’s faster, produces smaller bundles, and is the current official recommendation for new React projects.

Create a Vite + React project inside your WordPress theme directory:

# From wp-content/themes/
npm create vite@latest my-react-theme -- --template react
cd my-react-theme
npm install

Your theme directory should now look like this:

wp-content/themes/my-react-theme/
├── src/                  ← React source files
│   ├── App.jsx
│   ├── components/
│   └── main.jsx
├── public/
├── index.html
├── vite.config.js
├── package.json
├── style.css             ← WordPress theme header (required)
├── index.php             ← WordPress template entry point
└── functions.php         ← Enqueue scripts

Configure Vite to build into the theme directory

By default, Vite builds to a dist/ folder. Configure it to output assets directly into a subfolder of your theme, and disable filename hashing so functions.php can reference the output files with static names.

Edit vite.config.js:

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

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'assets',
    emptyOutDir: true,
    rollupOptions: {
      input: 'src/main.jsx',
      output: {
        entryFileNames: 'js/main.js',
        chunkFileNames: 'js/[name].js',
        assetFileNames: ({ name }) =>
          name?.endsWith('.css') ? 'css/main.css' : 'assets/[name][extname]'
      }
    }
  },
  server: {
    cors: true
  }
})

After running npm run build, Vite outputs to my-react-theme/assets/js/main.js and my-react-theme/assets/css/main.css — fixed paths your functions.php can reference reliably.

WordPress theme scaffold

WordPress requires two files to recognise a directory as a theme: style.css (with the theme header comment) and index.php (the default template).

style.css — theme header only:

/*
Theme Name: My React Theme
Theme URI: https://yourwebsite.com
Author: Your Name
Description: WordPress theme with React frontend
Version: 1.0.0
License: GPL v2 or later
Text Domain: my-react-theme
*/

index.php — outputs the HTML shell React mounts into:

<?php get_header(); ?>

<main id="root"><!-- React mounts here --></main>

<?php get_footer(); ?>

header.php:

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>

footer.php:

<?php wp_footer(); ?>
</body>
</html>

functions.php — enqueue the React build and pass WordPress data to it:

<?php

function my_react_theme_enqueue() {
    $theme_uri = get_template_directory_uri();
    $version   = wp_get_theme()->get('Version');

    wp_enqueue_script(
        'react-app',
        $theme_uri . '/assets/js/main.js',
        array(),
        $version,
        true  // load in footer
    );

    // Pass WordPress data to React via window.wpData
    wp_localize_script('react-app', 'wpData', array(
        'apiUrl'   => rest_url('wp/v2/'),
        'nonce'    => wp_create_nonce('wp_rest'),
        'siteUrl'  => get_site_url(),
        'themeUrl' => $theme_uri,
    ));

    wp_enqueue_style(
        'react-style',
        $theme_uri . '/assets/css/main.css',
        array(),
        $version
    );
}
add_action('wp_enqueue_scripts', 'my_react_theme_enqueue');

wp_localize_script makes window.wpData available in your React app. Using the REST API URL from rest_url() means your React code works correctly regardless of the WordPress installation path — no hardcoded domain names.

Fetch WordPress content from React

Update src/main.jsx to mount React to the #root element:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

Create src/components/BlogPosts.jsx:

import { useEffect, useState } from 'react'

function BlogPosts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    // Use the API URL passed by WordPress — no hardcoded domain
    const apiUrl = window.wpData?.apiUrl || '/wp-json/wp/v2/'

    fetch(`${apiUrl}posts?_fields=id,title,excerpt,link&per_page=10`)
      .then(res => {
        if (!res.ok) throw new Error(`API error: ${res.status}`)
        return res.json()
      })
      .then(data => {
        setPosts(data)
        setLoading(false)
      })
      .catch(err => {
        setError(err.message)
        setLoading(false)
      })
  }, [])

  if (loading) return <p>Loading posts...</p>
  if (error)   return <p>Error: {error}</p>

  return (
    <div className="blog-posts">
      {posts.map(post => (
        <article key={post.id} className="post">
          <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
          <a href={post.link}>Read more</a>
        </article>
      ))}
    </div>
  )
}

export default BlogPosts

Two notes on dangerouslySetInnerHTML: WordPress already sanitizes content server-side before it reaches the REST API, so using it here is safe for your own site’s data. If you’re ever rendering content from an external or user-controlled source, sanitize it client-side with DOMPurify first. The _fields query parameter limits the API response to only the fields you need, which speeds up the fetch.

Use BlogPosts in src/App.jsx:

import BlogPosts from './components/BlogPosts'

function App() {
  return (
    <div className="app">
      <BlogPosts />
    </div>
  )
}

export default App

Build and activate the theme

Run the production build:

npm run build

This outputs assets/js/main.js and assets/css/main.css into your theme directory. Activate the theme in WordPress (Appearance → Themes) and the React app loads and fetches your posts on every page visit.

For local development, run npm run dev to start Vite’s dev server with hot module replacement. You’ll access it directly at http://localhost:5173 — it’s disconnected from WordPress in dev mode, so mock your API calls or point apiUrl at your local WordPress installation. A production build is always required to test the full WordPress integration.

Deployment and CI/CD

The React build step needs to run before you deploy. The cleanest approach: add the build to your CI/CD pipeline so the compiled assets are always up to date on the server.

In a GitHub Actions workflow, add a build step before the SSH deploy:

- name: Install dependencies
  run: npm install
  working-directory: wp-content/themes/my-react-theme

- name: Build React app
  run: npm run build
  working-directory: wp-content/themes/my-react-theme

If the React source is separate from the WordPress repo (a common structure), the pipeline fetches both, runs the build, then deploys the compiled assets only. For the full CI/CD setup for WordPress themes, see the WordPress CI/CD pipeline guide.

Performance considerations

  • Code splitting: Vite handles code splitting automatically for dynamic imports. Use React.lazy() and Suspense for route-level splitting if your app grows beyond a single page.
  • API response caching: Cache REST API responses using the WordPress Transients API on the server side (a cached PHP endpoint reduces client-side fetch latency), or use React Query for client-side cache management.
  • Image optimisation: WordPress generates srcset attributes and WebP versions automatically. When displaying post featured images, use the _embed parameter in your API call to include media objects: posts?_embed&_fields=id,title,_embedded.
  • SEO limitation: Client-side React rendering means search engines receive an empty <div id="root"> on the initial HTML load. For a content-focused site where SEO matters, consider Next.js with the WordPress REST API as the data source — it provides server-side rendering (SSR) and static generation with the same component model. For an interactive app (dashboard, member area, single-page app) where SEO isn’t a primary concern, client-side rendering is fine.

If you want to use React within WordPress without the headless tradeoff — keeping full server-rendered HTML and SEO — build React blocks for the Gutenberg editor instead. The WordPress plugin with React JS guide covers that pattern. For managing PHP dependencies on the WordPress side of this architecture (Composer for theme dependencies), see the Composer for WordPress guide.

If you need a Node.js process alongside WordPress rather than React in the theme (for WebSockets, real-time data, or an Express backend), that’s a different architecture covered in the WordPress Node.js plugin guide. For the full developer tooling setup that complements this workflow, see 5 tools to streamline WordPress development.

Frequently asked questions

Yes. The approach is to build a React app (using Vite) inside your WordPress theme directory, configure Vite to output compiled assets to a known path (`assets/js/main.js`), and load those assets in `functions.php` using `wp_enqueue_script`. Your `index.php` template outputs a `

` mount point; React renders into it. WordPress data (posts, pages, custom post types) is fetched from the WP REST API using `fetch()`. This is sometimes called a 'headless-lite' or 'hybrid' approach: WordPress still handles the admin and content management, but the visitor-facing output comes from React components rather than PHP templates.

Use Vite. Create React App (CRA) was deprecated by the React team in early 2023 and is no longer officially maintained — it has known security issues in its dependencies and is not suitable for new projects. Vite is the current official recommendation: it starts in milliseconds, has native ES module support, produces smaller and faster production builds, and has first-class React support via `@vitejs/plugin-react`. Create a new project with `npm create vite@latest my-theme -- --template react`.

Use `wp_localize_script()` in `functions.php` after enqueuing your React script. This creates a JavaScript object before your script loads: `wp_localize_script('react-app', 'wpData', array('apiUrl' => rest_url('wp/v2/'), 'nonce' => wp_create_nonce('wp_rest')))`. In your React component, access it as `window.wpData.apiUrl`. This approach works regardless of the WordPress installation path and avoids hardcoding the domain name in React source files — hardcoded URLs break on staging environments or when the domain changes.

Yes, with a client-side rendering approach. When Google (or any search engine) loads your page, the initial HTML contains an empty `

` — the content only appears after JavaScript executes and the REST API fetch completes. Googlebot can render JavaScript, but the timing and reliability of this rendering is not guaranteed. For a content-focused site where organic search rankings matter, this is a significant limitation. The workaround is server-side rendering (SSR) using Next.js with the WordPress REST API as the data source — this generates full HTML on the server, which is crawlable immediately. For an interactive application (member dashboard, booking system, single-page app) where SEO is secondary, client-side React is fine.

Yes, for your own WordPress site's content. WordPress sanitizes post content, titles, and excerpts server-side before storing and serving them. The REST API serves this already-sanitized content, so using `dangerouslySetInnerHTML` to render `post.title.rendered`, `post.content.rendered`, or `post.excerpt.rendered` is safe. If you're ever rendering content from an external API, a user-submitted source, or any input you don't fully control, sanitize it client-side first using DOMPurify (`npm install dompurify`) before passing it to `dangerouslySetInnerHTML`.

A React WordPress theme (the headless-lite approach in this guide) uses React to render the entire visitor-facing frontend — the theme output is a React app that fetches WordPress data via the REST API. A Gutenberg block is a React component that runs inside the WordPress block editor, allowing content editors to add and configure it within the post/page editing interface. The block renders its output as standard WordPress HTML (server-side via a PHP `render_callback`, or as saved HTML), which is then served as part of the normal WordPress page. Gutenberg blocks are the right choice when you want to add a reusable, configurable component to the editor without changing the theme architecture.

Photo of Ajay Khandal

Written by Ajay Khandal

I'm a freelance WordPress developer with 14+ years of experience building, fixing, and speeding up sites for businesses, agencies, and store owners across the US, UK, Europe, and Australia. I specialize in custom themes, WooCommerce, and performance — the kind of work that shows up as faster load times and fewer support tickets. No account managers, no outsourced tickets — you work directly with me, with replies typically inside 5 hours.

Work with me →