Goal: Create WordPress plugin which will inject React based web app\widget into normal WordPress page\post\sidebar.
This is useful when you need to create an complex fully interactive widget\app like calculator for example, and you want it to be a part of WordPress page.
In this article I’ll describe one of the approaches, though it may be not perfect, we’ll dig deeper into other build approaches in next articles.
Let’s split our big task into sub-tasks to better understand how we achieve it:
- Create WordPress plugin
- it will enqueue our js\css and register
shortcode
to render root element of React app - yes shortcodes are old school approach (compared to Gutenberg Blocks) but they work well for this use case and we keep it simpler for now.
- it will also pass in App’s settings via data attribute of root element.
- it will enqueue our js\css and register
- Generate React app via
npx create-react-app
- the idea is to utilize a ready to use solution for scaffolding and Webpack configuration, so newbies like me 🙂 in react world could follow standard React tutorials to build his first React App.
- also you can use approach explained in this article to simply inject your existing React App, generated via
npx create-react-app
into WP based site.
- Modify build\start npm scripts to work well with WordPress.
- by default react build Webpack’s configuration will generate js files split in chunks – it makes it harder to link them via
wp_enqueue_scripts
as we used to in WordPress. - so we’ll write a code which will partially override config and Webpack will generate js\css which will be easy to use in WP.
- by default react build Webpack’s configuration will generate js files split in chunks – it makes it harder to link them via
- Write JSX\React code and build it.
Result plugin can be fetched from github https://github.com/MikhailRoot/embeded-react-app-in-wp
1. Create WordPress plugin
It will register a shortcode and enqueue scripts and styles for our React App.
https://github.com/MikhailRoot/embeded-react-app-in-wp/blob/master/plugin.php
NB: I use
define('ENV_DEV', true); // in wp-config.php
to load generated by yarn start
css\js files while development, set it to false
when you need to use css\js generated via yarn build
.
In a shortcode I pass in json string of settings as data-default-settings
attribute for our App’s root element.
2. Generate React App
In your new plugin’s directory simply run npx create-react-app app
and wait a bit.
We’ll use yarn add
for the sake of brevity but you can use npm
as well. Just pick one (anyway you can switch back any time if needed).
Let’s cleanup a bit via removing redundant files from src
directory of app. These are:
logo.svg
– because in WordPress we handle images URLs differently – you can use relative to plugin’s path links to images.serviceWorker.js
As well as references to them in index.js
and App.js
You can remove all files inside /app/public
except index.html
as we don’t need them.
Remove .git
directory inside react app’s folder – as you’ll probably want to track changes via git but for a whole plugin and not just react part of it. And in .gitignore
remove line
/build
So our main.js
and main.css
files which are result of build process will be tracked with git.
Next step is optional – we’ll install node-sass
I just prefer to work with SCSS files rather then plain CSS, so in case you like it as well – just cd
to your newly generated react app directory and then run yarn add node-sass
as described here https://create-react-app.dev/docs/adding-a-sass-stylesheet/
Create scss
folder under src
and move css files into it, renaming them to .scss
and change imports of these files in js to point to our new .scss
files like:
import './scss/App.scss';
in App.js
, and
import './scss/index.scss';
for index.js
3. Modify build\start npm scripts to work well with WordPress
In your React App’s directory, run yarn add rewire --dev
– we use rewire
to be able to redefine webpack.config provided by create-react-app
boilerplate without a need to eject
react app or modify config files which reside in node_modules
Now create new directory called scripts
– here we’ll set our JS files which will do magic for us.
start-non-split.js
– this file will change Webpack config on fly so that it will not split JS code in chunks and will not inject hash into filename.
const externals = require( './externals' );
const rewire = require( 'rewire' );
const defaults = rewire( 'react-scripts/scripts/start.js' );
const configFactory = defaults.__get__( 'configFactory' );
defaults.__set__( 'configFactory', ( env ) => {
const config = configFactory( env );
config.externals = {
...config.externals,
...externals,
};
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
config.optimization.runtimeChunk = false;
config.output.filename = 'static/js/[name].js';
config.output.chunkFilename = 'static/js/[name].js';
return config;
} );
build-non-split.js
– this is used for building production js\css files it will again build them as single files making it easier to link in WP.
const externals = require( './externals' );
const rewire = require( 'rewire' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const defaults = rewire( 'react-scripts/scripts/build.js' );
const config = defaults.__get__( 'config' );
/* we inject our Externals to keep our bundles clean and slim */
config.externals = {
...config.externals,
...externals,
};
/*
* we set jsonFunction of webpack to our custom one
* so multiple js bundles built with webpack could be safely loaded,
* as we are aware that our plugin isn't the only one which can be potentially built with webpack.
*/
config.output.jsonpFunction = 'YourReactApp_webpackJsonpjs';
/* we disable chunks optimization because we want single js\css file to be loaded by plugin. */
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
config.optimization.runtimeChunk = false;
config.output.filename = 'static/js/[name].js';
config.output.chunkFilename = 'static/js/[name].js';
/*
* lets find `MiniCssExtractPlugin` type of object in plugins array and redefine it's options.
* And remove all unnecessary plugins.
*/
const disabledPlugins = [
'GenerateSW',
'ManifestPlugin',
'InterpolateHtmlPlugin',
'InlineChunkHtmlPlugin',
'HtmlWebpackPlugin',
];
config.plugins = config.plugins.reduce( ( plugins, pluginItem ) => {
if ( disabledPlugins.indexOf( pluginItem.constructor.name ) >= 0 ) {
return plugins;
}
if ( pluginItem instanceof MiniCssExtractPlugin ) {
plugins.push(
new MiniCssExtractPlugin( {
filename: 'static/css/[name].css',
chunkFilename: 'static/css/[name].css',
} )
);
} else {
plugins.push( pluginItem );
}
return plugins;
}, [] );
externals.js
– this file contains shared scripts which can be provided by WP Core so we don’t need them to be bundled into our build.
module.exports = {
react: 'React',
'react-dom': 'ReactDOM',
};
In my case after I’ve marked React
and ReactDOM
as external ones JS bundle’s size decreased from 160kb to 56kb – those get’s loaded from wp-includes
as a dependency any way, but imagine you have bunch of such Apps and each will load extra version of bundled React library. To overcome this we mark them as external – this way we keep our bundles clean and light, and at the same time we leverage WordPress scripts dependency manager to link necessary dependencies for us.
Then open package.json
file and in scripts
section update start
command to
"start": "node ./scripts/start-non-split.js",
and build
command to
"build": "node ./scripts/build-non-split.js"
So your package.json
file will look like this:
{
"name": "app",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "node ./scripts/start-non-split.js",
"build": "node ./scripts/build-non-split.js",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"node-sass": "^4.14.0",
"rewire": "^5.0.0"
}
}
4. Write JSX\React code and build it.
Now we are almost ready, let’s update index.js
file which initializes our React app on root element inserted via shortcode.
import React from "react";
import ReactDOM from "react-dom";
import "./scss/index.scss";
import App from "./App";
const rootEl = document.getElementById("md-react-app");
if (rootEl) {
const settings = JSON.parse( rootEl.getAttribute( 'data-default-settings' ) );
ReactDOM.render(
<React.StrictMode>
<App settings={settings} />
</React.StrictMode>,
rootEl
);
}
Here we also read initialization settings data provided by WP, and pass them into App as prop.
If you’ll use Redux you can use these data to generate initial state for App.
Let’s update our App.js
file to utilize our settings from props.
import React from 'react';
import './scss/App.scss';
function App(props) {
const items = props.settings.some_items || [];
return (
<div className="App">
<header className="App-header">
<h1>{ props.settings.l18n.main_title }</h1>
<ul>
{
items.map((item, index)=>{
return (
<li key={index}>{item}</li>
)
})
}
</ul>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
Now we are ready to test everything together.
- activate our plugin and place
[md-react-app]
shortcode on page. - in terminal
cd
to plugin’s directory/app
and runyarn build
– it will build js\css then just open a page where you’ve placed a shortcode and app will be rendered there.
For development:
- add
define('ENV_DEV', true);
to yourwp-config.php
- in plugin’s
/app
sub directory runyarn start
it will build css\js and spin up a dev server to serve updated css\js whenever you make change in source files. (it doesn’t support live reloading yet but for quickstart it’s enough). - It will also open a new tab in browser leading to localhost:3000 and showing error message that
React is not defined
just close and ignore it. It says React is not defined just because we’ve declared it as external and we haven’t bundled it into our resultmain.js
- It will work inside WordPress page because we’ve declared
wp-element
as dependency in our plugin, so WordPress will load React on it’s own. - So just refresh your WordPress page where you’ve placed our new shortcode and here’s your React App inside WordPress page.
- NB: in
ENV_DEV true
mode you need toyarn start
otherwise it will not be able to load css\js, set it tofalse
or comment out for production.
Happy codding 🙂
Damn that’s a freakin awesome tutorial.
Learned a lot.
Thanks a bunch.
I’m getting an error as
WebSocket connection to ‘ws://localhost/sockjs-node’ failed: Error during WebSocket handshake: Unexpected response code: 404
How can i fix this?
It’s live reloading feature says it can’t connect, I’m sorry but I didn’t have time to solve it yet. In my understanding the problem with current live reloading implementation is: it works like this -> normally webpack injects live reloader js code into page, where the app is (into index.html) so we need to figure out how we can inject it from webpack with wordpress. Still this error doesn’t stop you from just CMD+R\Ctrl+F5 or refreshing page to see changes while developing app. I’ll tinker on this on spare time later, but you can add PR to github if you’ll figure out solution quicker.
That’s a real cool tutorial, but the start script won’t work for me. It looks like it even doesn’t get used at all. Any advices? Has that something to do with the dev_env? What does this configFactory do?
In my build I get also an error “Target container is not a DOM element.” in the browser. Do you also get this error?
Thanks in advance for your help and especially for this tutorial!
–Jö–
Good day Stefan!
1) if script generates single js and css files for you it works, please make sure you’ve made changes to `scripts` section in `package.json` as they are change how commands `yarn start` and `yarn build` work our way.
2) `configFactory` is part of `create-react-app` webpack based build toolchain, what our script does is it takes this configuration object, updates it to work the way we want it.
3) `Target container is not a DOM element.` error might mean that you’ve forgot to place a shortcode into page, which will render your div to contain app.
Hello Mikhail.
I tried to do everything as you instructed but I get error: Fatal error: Uncaught Error: Call to undefined function get_plugin_data() in C:\wamp64\www\wordpress\wp-content\plugins\plugin\plugin.php on line 47
What I am doing wrong 🙂
Jani
Just clone repository from github as is into /wp-content/plugins directory so result path will be `/wp-content/plugins/embeded-react-app-in-wp/` – activate plugin and it should work.
Also you can check this thread about this kind of issue https://wordpress.stackexchange.com/questions/17948/call-to-undefined-function-get-plugin-data
The thing is for me it always work (latest WP), so i can’t reproduce your issue. Anyway if nothing will help you just replace this single line 47 with `$ver = “your-plugin-version-name”;` and it will eliminate the need to get version from plugin’s data.
Hello, thanks for this wonderful post. I was able to set up a react page inside my wordpress website. However, when I tried to add a dependency to the project (@material-ui/core), I got this CORS error in the console and the site won’t load:
Access to fetch at ‘http://localhost:3000/static/js/main.js?ver=2020-07-27-10-04-33’ from origin ‘http://localhost:8888’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.
Any help would be appreciated. Thanks again!
Thanks for your help. This boilerplate works great.
I was wondering if this could work with multiple react apps. I created two react app using your boilerplate, and assigned them to their own separate pages with different URL.
When I assign a href to route the URL to the other react app, it keeps redirecting to the current one; I have both plugins activated and are running in localhost:3000 and 3001
Is there a way around this?
In production mode you can have as many apps as needed even multiple apps on same page, just set different shortcodes for them, also build js in production mode for each app (via `npm run build`) and set ‘ENV_DEV’ to false so it will not try to load resources from dev mode webpack’s server at localhost:3000, or in case you want to have both of them in dev mode, run both and adjust port number on each plugin, like you’ve mentioned one was localhost:3000 and another one was localhost:3001 -> webpack assigned next free port to it. (Probably in your case in dev mode you’ve forgot to change port in PHP for respective plugin, this is why it still loads assets css\js from localhost:3000 instead of localhost:3001)
Ah thanks for reminding about that, I forgot to adjust the code below in in plugin.php to 3001
if ( defined( ‘ENV_DEV’ ) && ENV_DEV ) {
// DEV React dynamic loading.
$ver = gmdate( ‘Y-m-d-h-i-s’ );
$js_to_load = ‘http://localhost:3001/static/js/main.js’;
$css_to_load = ‘http://localhost:3001/static/css/main.css’;
}
Thank you very much for this tutorial, that’s been a great help!
One query: for the code
const rootEl = document.getElementById(“md-react-app”);
… for localhost:3000, this generates an empty page as it expects an ID of “root”. Do I need to manually change the ID between production (md-react-app) / development (root) use; or is there something I’ve missed?
PS: I’m using WAMP on Mac for WP development
Posted too early. The missing step was to update the container ID in /public/index.html
Is there any workaround or any way to do this with react.js using the below code?
The problem that I’m having is that I can’t just call ” wp_nonce_field( ‘wp_rest’ ); ” in React.
I want to send a POST requests to change data which uses ACF / acf-to-rest-api plugin (WordPress).
————————————————–
https://github.com/airesvsg/acf-to-rest-api#editing-the-fields
Site:
Save
The solution is to use wp_localize_script function -> it will create global variable with JS within window so in your React app you can refer to it like window.YOUR_GLOBAL_NAME.nonce to get nonce value generated with wp_create_nonce(‘wp_rest’).
Would I be right in assuming this approach requires that node etc. be set up on the server where wordpress is running?
I have a simple React app that I built a few years ago and things were as simple as having a tiny plugin that loads “bundle.min.js”. This worked very well as I could just hand off this one file to a colleague who handles all the WP stuff on his shared server.
Revisiting the situation today, trying to just load “bundle.min.js” leaves us with an empty (ie. blank screen if it were loaded on a basic html page).
This is WP 5.7 and my understanding is that WP includes React these days. Yet I’d try this method and main.js causes “React not defined.”
Suggestions??
This approach doesn’t require server side node js installed, here we use React just to build widget and manage DOM for us, we don’t rely on server side rendering, database access and other features which can be a part of fully fledged React based apps.
Yes, React now is base for Gutenberg, you can use wordpress scripts to build main entry point js file https://www.npmjs.com/package/@wordpress/scripts . It will generate bundle and php file which will contain dependencies and version which you can use when linking js via wp_enqueue_script().