Develop a WordPress Plugin Using Webpack and React

WordPress + Webpack + React

Ghost Inspector is an automated browser testing tool for continuously monitoring websites. We recently released our WordPress plugin to show test results inside your WordPress admin dashboard. In this tutorial, you will learn how to build your own plugin using React, Webpack, and the Ghost Inspector API. You can view the final source code on GitHub.

Why Use React?

Going back to server-side rendered markup with PHP feels like stepping out of a time machine. Fortunately, the modern frontend stack can be integrated with PHP fairly easily. Although Angular is great for large applications, React is probably more suited for smaller components like we’re going to build for this plugin. You could just as easily use Angular, or Vue, but I’m personally a big fan of React, so that’s what we’re using!

Building with React means the codebase is familiar to other React developers and much more familiar to JavaScript developers than a bunch of PHP would be. That familiarity, along with the popularity of React, means there will be more libraries to help speed up development and more answers to problems when we run into them.

What Are We Building?

A plugin for WordPress that will display a widget on the admin dashboard. Just like our official WordPress plugin, the widget you build will display data from the Ghost Inspector API, but this approach can be used as a template for any widget using any API(s). We will use:

To get started, create a new folder for your plugin and call it whatever you like. In the root of this folder is where your plugin’s PHP code will live.

Getting Started With React

Bootstrapping With create-react-app

While you don’t have to know React to complete this tutorial, it will definitely help. I will try to cover all the basics, but I may gloss over details that you’ll need to learn separately. I’m going to lean on create-react-app to bootstrap our frontend app. If you’re not familiar with it, this will be a great chance to learn. It will help speed up the development process. However, if you prefer you can still use your own setup with Webpack (or anything else for that matter), but this tutorial will assume you’re using create-react-app.

TIP: with npx you can use create-react-app without installing it globally and npx is included in npm 5.2+!
npx create-react-app frontend

You will want to put your React code into a subfolder of your plugin. This will make it easier to organize the frontend app separate from the WordPress and build specific code. In the above example, I’m calling that folder “frontend”, but you can name it anything. Note that create-react-app will make a “public” folder within the main folder, so you might want to avoid using that. Once the script is finished, you can start your React app with:

cd frontend
npm start

Then open http://localhost:3000/ to see it working! This is using webpack-dev-server, so the built files don’t exist on the filesystem yet. During development, we can still point WordPress to load the files from the localhost server, but for production you’ll use npm run build to generate a bundle that will live on the same server as the WordPress installation.

Building Out The Dashboard Widget React Component

We’re going to get rid of the example app and create our own. The goal of this tutorial is to create a dashboard widget that will call the Ghost Inspector API to get a list of tests within a given suite. Our dashboard widget will consist of a table listing the name, status, and last run date for each test within a suite. We’re not doing anything fancy here with sorting or pagination, but it’s a good start.

First, let’s create the table with the assumed data that we’ll get as a response from the API. You can overwrite the existing App.jsx file or create a new one called Dashboard.jsx. Here’s the code for the component:

import React  from 'react'
import format from 'date-fns/format'

const Dashboard = ({ suiteId, apiKey }) => {
    const tests = [] // placeholder until we fetch from API
    const suite = {} // placeholder until we fetch from API
    const total = tests.length
    const totalPassing = tests.filter(test => test.passing === true).length
    return (
        <div className="ghost_inspector_wrapper">
            <p className="ghost_inspector_header">Latest results for suite: <a href={`${suiteId}`} target="_blank" rel="noopener noreferrer" className="ghost_inspector_suite_name">{}</a> ({totalPassing}/{total} passing)</p>
            <div className="ghost_inspector_tests">
                    <th>Test Name</th>
                    <th>Last Run</th>
                { => (
                <tr key={test._id}>
                    <a href={`${test._id}`} target="_blank" rel="noopener noreferrer">{}</a>
                    <td className="ghost_inspector_status">
                    <span className={`dashicons dashicons-${test.passing ? 'yes' : 'no'}`}></span>
                    {format(new Date(test.dateExecutionFinished), 'MMM D')}

view in source code

In this code we’re using a function as a component, since there’s no need for a Class yet (or ever really), but you could write it that way if you prefer.

You can see in the example code that we’re expecting tests to be an array of objects. We’re using Array.filter to get a count of the total passing. Along with a link to the suite, we display the total passing out of the total tests above the table. We’re expecting the suiteId to be passed in via props. Eventually that will come from WordPress database, but for now you can hard-code it. Within the tbody of the table, we loop through the tests and for each one display:

If you want to see the UI in action, you can put in some dummy content (or even pass it via props). In a bit we’ll actually pull this data from the API. Here’s an example test object:

  _id: 'abc123',
  passing: true,
  dateExecutionFinished: '2017-07-13T21:43:14.140Z',
  name: 'Example test'

view a complete test object in our API docs

Getting Real Data From Ghost Inspector API

Now that we have our UI in place, let’s populate it with real data from our API (you’ll need an account and at least one test). Since we’re still running locally and outside of WordPress, we can just request the data directly. Later on, we’ll add a proxy to go through WordPress.

For these examples, I’m going to use hooks (available in React 16.8) to add state and side effects. You could also do this with Class state and component lifecycle methods. Let’s add some state for the data we’ll be fetching from the API. Update your import from 'react' to include React.useState and inside your Dashboard component (at the top), add the following:

const [tests, setTests] = useState([])

view in source code

This is essentially the same as this.state = { tests: [] }. The function setTests is a way to update the state for tests only. Now let’s add a method for getting the tests. Update your import again to include React.useEffect and add a new method to your Dashboard component:

const fetchTests = async (suiteId, apiKey) => {
  try {
    const response = await fetch(`${suiteId}/tests/?apiKey=${apiKey}`)
    const json = await response.json()
    if (json.code) === 'SUCCESS') {
    } else {
      throw new Error(json.message)
  } catch (error) {

view in source code

We’re using the fetch API, async/await, and a try/catch block. However you could just as easily use axios or any other promise based HTTP client. This function takes a suite ID and an API key as arguments and makes a request to the API. If it successfully gets data, it uses our setTests function to update the state, otherwise it logs an error. Now to call this method add this to your Dashboard component:

useEffect(() => {
  if (suiteId && apiKey) {
    fetchTests(suiteId, apiKey)
}, [])

view in source code

We’re passing an empty array as the second argument for useEffect because we only want to run this effect once, essentially the same as componentDidMount. We’re using the component props suiteId and apiKey. This is basically all you need to get the tests rendered in your component.

Example widget on dashboard

Eventually you’ll want to add more state for loading and error messages. You can see those in action in the full source code.

Getting Started With WordPress

Running Locally With Docker

I will be using Docker stack to bootstrap a local WordPress installation. If you already have WordPress running locally, you can skip to the next section. You may already have a WordPress installation running locally with XAMP, which is fine. The specific way you run WordPress does not matter, but you definitely will want a local version while developing your plugin. I have some tips for working with the Docker stack specifically, but most of this tutorial can work with any setup.

First, make sure you have Docker installed. You can use official WordPress image, but that does not include MySQL for the database. You could install that locally or create another Docker image manually, but with Docker Stack there’s a much simpler way to get everything running with a single command.

You can copy the stack config from our WordPress plugin repository into your local plugin folder and then run:

docker stack deploy -c stack.yml wordpress

This will start a Docker swarm with WordPress and MySQL configured with enough defaults that all you need to to do is visit http://localhost:8080 to finish the installation.

Starting The Plugin

We’re going to use WordPress hooks to add some HTML markup when our widget is installed. This will be the div container that our React app renders into. Throughout the PHP code, we’re going to be using function names and HTML IDs with the ghost_inspector prefix, since that is what our plugin uses. When you build your own plugin, you’ll want to change these to something unique (and long). Our first submission to the WordPress plugin directory was rejected because we used gi_ as a prefix in a few places. This was the review team’s response:

Don’t try to use two letter slugs anymore. We have over 60 THOUSAND plugins on alone, you’re going to run into conflicts.

add_action('wp_dashboard_setup', function () {
    wp_add_dashboard_widget('ghost_inspector_widget', 'Ghost Inspector', 'ghost_inspector_display_widget');
    function ghost_inspector_display_widget() {
        <div id="ghost_inspector_dashboard"></div>

view in source code

We’re using the hook WordPress function add_action, which takes a hook name and a callback function. We’re using the hook wp_dashboard_setup, which is when we want our dashboard widget to be added. Inside our callback function, we use the WordPress function wp_add_dashboard_widget to add our widget.

Save this file as “ghost-inspector.php”. To install the plugin for the first time, create a zip of your php file and follow the steps for manual plugin installation by uploading a zip archive. Going forward, you can just edit the file directly, or if you’re using Docker, copy the file into your instance (adjust for your local setup):

docker cp [local-repo-location]/ghost-inspector.php [your-wordpress-container-id]:/var/www/html/wp-content/plugins/ghost-inspector/ghost-inspector.php

If you install your plugin now, you should see your new (empty) widget added to the dashboard. To get the React app to render, we’ll need to add the JavaScript files that are built by create-react-app. In WordPress terms, this is called enqueuing scripts

add_action('admin_enqueue_scripts', function ($hook) {
  // only load scripts on dashboard
  if ($hook != 'index.php') {
  if (in_array($_SERVER['REMOTE_ADDR'], array('', '::1'))) {
    // DEV React dynamic loading
    $js_to_load = 'http://localhost:3000/static/js/bundle.js';
  } else {
    $js_to_load = plugin_dir_url( __FILE__ ) . 'ghost-inspector.js';
    $css_to_load = plugin_dir_url( __FILE__ ) . 'ghost-inspector.css';
  wp_enqueue_style('ghost_inspector_css', $css_to_load);
  wp_enqueue_script('ghost_inspector_js', $js_to_load, '', mt_rand(10,1000), true);

view in source code

In this code example, by using the hook admin_enqueue_scripts it only runs in the admin (not the public pages). We also check if the specific page is the dashboard and if not, return early. The next few lines use the PHP variable $_SERVER['REMOTE_ADDR'] to conditionally load our React script from webpack-dev-server on localhost. The array value we’re checking for in this case is the Docker IP. In production, it will load the built files (we’ll get to this later). Props to Victor Gerard Temprano for this awesome bit of code.

TIP: To get a single bundle.js, disable code splitting in create-react-app.

Building a Proxy for the Ghost Inspector API

WordPress requires all ajax requests to go through wp-admin/admin-ajax.php. Plus, we don’t want to send the API key through the browser, so we’ll need to build a proxy in PHP using the WordPress REST API. Our React widget will make requests to custom endpoints, which will then make server-side requests to and return the results back to the client. You can use this same pattern for any third party API(s) you want to incorporate with your widget.

function ghost_inspector_api_proxy($request) {
  // first we get the query parameters from the request
  $params = $request->get_query_params();
  // we add the API key to the params we'll send to the API
  $params['apiKey'] = 'your_api_key_here'
  // we get the endpoint since we'll use that to construct the URL
  $endpoint = $params['endpoint'];
  // delete the endpoint since we no longer need it in the params
  // convert the params back to a string
  $query = http_build_query($params);
  // build the URL using the endpoint and any params and make a remote GET request
  $request = wp_remote_get("$endpoint?$query");
  // get the body from the response and return it as a JSON object
  return json_decode(wp_remote_retrieve_body($request));

view in source code

The above code is getting the query parameters from the http request. It’s also pulling out and removing the endpoint param because that’s what we’ll use to build the URL for the API, but we don’t want to send it along with any other parameters. For now we’re just hard-coding the API key, but eventually you’ll want to pull this from from WP options. Then it stuffs the query back into a URL string and uses wp_remote_get to make a request to a remote URL. It gets the body from the response and returns it. This is all you need to proxy requests from WordPress to a third party API!

Now that we have a proxy function in place, we need to tell WordPress to create a new custom endpoint that our client side code can access:

add_action('rest_api_init', function () {
  register_rest_route('ghost-inspector/v1', '/proxy', array(
    // By using this constant we ensure that when the WP_REST_Server changes our readable endpoints will work as intended.
    'methods'  => WP_REST_Server::READABLE,
    // Here we register our callback. The callback is fired when this endpoint is matched by the WP_REST_Server class.
    'callback' => 'ghost_inspector_api_proxy',

view in source code

This adds a new endpoint that can be called anywhere in WordPress. We specify what methods are available (in this case just GET) and give it the function to call (our proxy function from before). Now you can update your React app to use this endpoint instead of the URL.

To get the URL, you can have WordPress output that on the page. Update your admin_enqueue_scripts hook in ghost-inspector.php to include this block:

wp_localize_script('ghost_inspector_js', 'ghost_inspector_ajax', array(
    'urls'    => array(
      'proxy'    => rest_url('ghost-inspector/v1/proxy'),
    // if you are going to add forms to your widgets, you'll need this:
    'nonce'   => wp_create_nonce('wp_rest'),

view in source code

This uses the same string we used for wp_enqueue_script. It will inject some data onto window.ghost_inspector_ajax. We’ve added the proxy URL. There’s also a nonce, which you can ignore for now. Eventually, if you allow the user to submit data through your plugin, the nonce needs to be sent with every ajax request. Read more about using nonces in WordPress.

You can update your fetchTests function in the Dashboard component to use the WordPress proxy URL instead. Add the endpoint as a parameter and remove the API key (since it’s now being populated in PHP).

const response = await fetch(`${window.ghost_inspector_ajax.urls.proxy}?endpoint=/v1/suites/${suiteId}/tests/`)
TIP: To support different permalink settings, you can conditionally include the ? with the following code:
urls.proxy.indexOf('?') > -1 ? '&' : '?'
view in source code

That’s it! You should now have a functioning plugin. Keep reading to add some final touches.

Final Touches

The next step for your plugin would be adding a settings component that would allow the user to save their API key and suite ID. I won’t cover that here, but you can see an example in the source code.


We didn’t cover permissions, but there are some methods built into WordPress to help with that. It’s a good idea to add a permission_callback to your REST routes. You can see how we’re using that in the final plugin code.

Uninstall Hook

With WordPress plugins, it’s a good idea to cleanup anything you’ve changed when the user uninstalls. Since we didn’t use the WordPress database, there’s not anything to clean up from this tutorial, but eventually you may use the options table, etc. You can see an example of deleting options in the final plugin code.


Hopefully you’ve learned how to create and quickly iterate on a WordPress plugin using React, Webpack, and Docker. If you are interested in learning more, you can study the final plugin code. Following our basic setup, you should be able to create your own plugin using any API(S). If you’re having trouble with WordPress/React/Webpack, here are some other great tutorials that helped me:

Jordan Kohl

Author: Jordan Kohl

Jordan has been building websites since he got a 486 for Christmas. He is passionate about writing eloquent JavaScript and streamlining the development process. He enjoys hiking with his family, building LEGO, and drinking good brews (coffee and beer).

Welcome to Our Blog!

Ghost Inspector is an automated browser testing and monitoring service. Here you'll find testing and QA related blog posts written by our team members. Subscribe to stay up to date with our latest posts!

Popular Blog Posts

See All Blog Posts