You’ve built a pricing calculator, a booking widget or an interactive chart in React. Now the client wants it on their WordPress site, on one specific page, styled to match. Here’s how to do it properly with a small plugin and a shortcode, and how to avoid the problems that usually show up a week later.
Step 1: Build React to predictable file names
WordPress needs to know which files to load. By default, Vite adds a hash to every file name, which changes on each build. Tell it to output fixed names and a single bundle instead:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
rollupOptions: {
input: 'src/main.jsx',
output: {
format: 'iife',
entryFileNames: 'app.js',
assetFileNames: 'app.[ext]',
},
},
},
});
The iife format produces a plain script that runs in any browser without type="module", which keeps the WordPress side simple.
Step 2: Mount on a container, not on #root
A normal React app mounts on a single #root element. On WordPress you might want the same app twice on one page, so mount on every matching container and read props from a data attribute:
// src/main.jsx
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
document.querySelectorAll('[data-wotc-app="pricing"]').forEach((el) => {
const props = JSON.parse(el.dataset.props || '{}');
createRoot(el).render(<App {...props} />);
});
Step 3: Register a shortcode in a small plugin
Create a folder in wp-content/plugins/, copy your dist folder into it, and add a main plugin file:
<?php
/**
* Plugin Name: Pricing App Embed
*/
add_shortcode( 'pricing_app', function ( $atts ) {
$atts = shortcode_atts( [ 'currency' => 'EUR' ], $atts, 'pricing_app' );
$base = plugin_dir_url( __FILE__ ) . 'dist/';
wp_enqueue_style( 'pricing-app', $base . 'app.css', [], '1.0.0' );
wp_enqueue_script( 'pricing-app', $base . 'app.js', [], '1.0.0', true );
return sprintf(
'<div data-wotc-app="pricing" data-props="%s"></div>',
esc_attr( wp_json_encode( $atts ) )
);
} );
Activate the plugin, then add [pricing_app currency="USD"] to any page. Because the assets are enqueued inside the shortcode callback, they load only on pages that use it. Loading the script in the footer means the container already exists when React runs.
The pitfalls that break most embeds
- CSS leaking both ways. Tailwind’s base reset or a global
bodyrule in your app will restyle the theme. Scope your styles under a wrapper class, or disable global resets. Watch for the opposite too: theme styles onbuttonorinputleaking into your app. - Stale caches. If you keep the version at
1.0.0, visitors and caching plugins may serve the old bundle after you deploy. Bump the version on every build, or usefilemtime()on the file. - Talking to WordPress. If your app calls the REST API, pass
rest_url()and a nonce fromwp_create_nonce( 'wp_rest' )into the props, and send the nonce in anX-WP-Nonceheader. - Two apps, two copies of React. Each separately bundled app ships its own React. That’s fine for one or two widgets, but heavy if you embed many different apps on the same page.
The shortcut
The approach above works, but you’ll repeat it for every app and every client: a new plugin, a new shortcode, new version bumps. We built Shortcode Embed for React Apps to remove that work. Upload your build from the dashboard, give it a name, and place it with [react_embed app="pricing"]. Props, conditional loading and cache-busting are handled for you.
Either way, you now know what’s going on under the hood, which is the best defense when something breaks at 5 p.m. on a Friday.