WordPress REST API

How to build React Apps on top of the wordpress REST API:

WordPress is a powerful content management tool. But when it comes to developing on top of it, can be quite frustrating. WordPress’s crazy mixture of HTML and PHP loops can often prove unintuitive to grasp and hard to maintain.

There’s a light at the end of the tunnel, though! Starting with version 4.7, WordPress comes with a built-in REST API, and plugins are no longer required. This makes it easier to use WordPress strictly as a backend data storage or CMS, while allowing for a totally custom front end solution of your choice.

You no longer need to have a local WordPress installation and deal with setting up vhosts. Your local development process can be limited to building the front end that is connected with a WordPress installation hosted on a remote server.

In this article I’m going to use ReactJS to build the front end part of the application, React Router for routing, and Webpack for bundling it all together. I’ll also show you how to integrate Advanced Custom Fields, for those of us who have come to rely on it as a tool to create an intuitive solution for our clients.

The stack looks like this:
– ReactJs
– React Router v4
– Alt (for Flux implementation)
– Webpack v2

GitHub repohttps://github.com/DreySkee/wp-api-react
React frontend demo urlhttp://wp-api-react.surge.sh/
WordPress backend demo urlhttp://andreypokrovskiy.com/projects/wp-api-react/wp-admin
User: demo
Pass: wp-react-demo

Project setup

Let’s name the project “wp-api-react”. To follow along, first thing you need to do is include this in your package.json and run npm install:

{
  "name": "wp-api-react",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --inline --progress --config webpack.dev.js",
    "build": "npm run clean && webpack -p --progress --config webpack.production.js",
    "clean": "rimraf ./build/*"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "alt": "^0.18.6",
    "axios": "^0.16.2",
    "babel-polyfill": "^6.23.0",
    "babel-runtime": "^6.23.0",
    "lodash": "^4.17.4",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "react-router-dom": "^4.1.1"
  },
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-loader": "^7.0.0",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "html-webpack-plugin": "^2.28.0",
    "react-hot-loader": "^1.3.1",
    "rimraf": "^2.6.1",
    "webpack": "^2.6.1",
    "webpack-dev-server": "^2.4.5"
  }
}

Install webpack and webpack-dev-server globally as well if you have not done this already:

npm i webpack webpack-dev-server -g

Now in the project folder create wepack.dev.js for development configuration and webpack.production.js with configuration for building the project for production.

Paste this in webpack.dev.config.js:

var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {  
    devtool: 'cheap-module-source-map',
    devServer: {
        historyApiFallback: true, // This will make the server understand "/some-link" routs instead of "/#/some-link"
    },
    entry: [
        'babel-polyfill',
        'webpack-dev-server/client?http://127.0.0.1:8080/', // Specify the local server port
        'webpack/hot/only-dev-server', // Enable hot reloading
        './src/scripts' // This is where Webpack will be looking for the entry index.js file
    ],
    output: {
        path: path.join(__dirname, 'build'), // This is used to specify folder for producion bundle
        filename: 'bundle.js', // Filename for production bundle
        publicPath: '/'
    },
    resolve: {
        modules: [
            'node_modules', 
            'src',
            path.resolve(__dirname, 'src/scripts'),
            path.resolve(__dirname, 'node_modules')
        ], // Folders where Webpack is going to look for files to bundle together
        extensions: ['.jsx', '.js'] // Extensions that Webpack is going to expect
    },
    module: {
        // Loaders allow you to preprocess files as you require() or “load” them. 
        // Loaders are kind of like “tasks” in other build tools, and provide a powerful way to handle frontend build steps.
        loaders: [
            {
                test: /\.jsx?$/, // Here we're going to use JS for react components but including JSX in case this extension is preferable
                include: [
                    path.resolve(__dirname, "src"),
                ],
                loader: ['react-hot-loader']
            },
            {
                loader: "babel-loader",

                // Skip any files outside of your project's `src` directory
                include: [
                    path.resolve(__dirname, "src"),
                ],

                // Only run `.js` and `.jsx` files through Babel
                test: /\.jsx?$/,

                // Options to configure babel with
                query: {
                    plugins: ['transform-runtime'],
                    presets: ['es2015', 'stage-0', 'react'],
                }
            }
        ]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(), // Hot reloading
        new webpack.NoEmitOnErrorsPlugin(), // Webpack will let you know if there are any errors

        // Declare global variables
        new webpack.ProvidePlugin({
            React: 'react',
            ReactDOM: 'react-dom',
            _: 'lodash'
        }),

        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: './src/index.html',
            hash: false
        })
    ]
}

And this in webpack.production.config.js:

var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {  
    devtool: 'source-map',
    devServer: {
        historyApiFallback: true, // This will make the server understand "/some-link" routs instead of "/#/some-link"
    },
    entry: [
        './src/scripts' // This is where Webpack will be looking for the entry index.js file
    ],
    output: {
        path: path.join(__dirname, 'build'), // This is used to specify folder for producion bundle
        filename: 'bundle.js', // Filename for production bundle
        publicPath: '/'
    },
    resolve: {
        modules: [
            'node_modules', 
            'src',
            path.resolve(__dirname, 'src/scripts'),
            path.resolve(__dirname, 'node_modules')
        ], // Folders where Webpack is going to look for files to bundle together
        extensions: ['.jsx', '.js'] // Extensions that Webpack is going to expect
    },
    module: {
        // Loaders allow you to preprocess files as you require() or “load” them. 
        // Loaders are kind of like “tasks” in other build tools, and provide a powerful way to handle frontend build steps.
        loaders: [
            {
                test: /\.jsx?$/, // Here we're going to use JS for react components but including JSX in case this extension is preferable
                include: [
                    path.resolve(__dirname, "src"),
                ],
                loader: ['react-hot-loader']
            },
            {
                loader: "babel-loader",

                // Skip any files outside of your project's `src` directory
                include: [
                    path.resolve(__dirname, "src"),
                ],

                // Only run `.js` and `.jsx` files through Babel
                test: /\.jsx?$/,

                // Options to configure babel with
                query: {
                    plugins: ['transform-runtime'],
                    presets: ['es2015', 'stage-0', 'react'],
                }
            }
        ]
    },
    plugins: [
        new webpack.NoEmitOnErrorsPlugin(), // Webpack will let you know if there are any errors

        // Declare global variables
        new webpack.ProvidePlugin({
            React: 'react',
            ReactDOM: 'react-dom',
            _: 'lodash'
        }),

        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: './src/index.html',
            hash: true
        }),

        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            },
            sourceMap: true
        }),
    ]
}

Create “src” folder in the project root and create index.html inside of it. The index.html file will include this chunk of code:

<!DOCTYPE html>
<html>
<head>
    <title>React with WordPress API</title>
</head>
<body>
    <div id="app"></div> 
</body>
</html>

Now let’s add a few more folders to the project. Inside of the “src” folder create “scripts” folder and inside of “scripts” create “components”, “flux” and index.js file. This structure will help to keep files organized.

By now the folder structure should look like this:

wp-api-react/
 — node_modules/
 — src/
 — — scripts/
— — — components/
 — — — flux/
 — — — index.js
 — — index.html
— package.json
— webpack.config.js

index.js is the entry point for Webpack and it will hold all routes for the project. Let’s include React, React Router and basic routing structure in the file:

import {render}             from 'react-dom';
import Home                 from 'components/Home.js';

import {
    BrowserRouter as Router,
    Route,
    Redirect,
    Switch
} from 'react-router-dom';

class AppInitializer {

    run() {
        render(
            <Router>
                <div>
                    <Switch>
                        <Route path="/" component={ Home } exact />
                        <Route render={() => { return <Redirect to="/" /> }} />
                    </Switch> 
                </div>
            </Router>

            , document.getElementById('app')
        );
    }
}

new AppInitializer().run();

index.js is referencing Home component in imports. We need to create it in the “components” folder. Home.js will be the template component for the homepage. Include this in the file:

class Home extends React.Component {
    render() {
        return (
            <div>
                <h2>Hello world!</h2>
            </div>
        );
    }
}

export default Home;

If you run npm start in the terminal inside of the project folder and open http://localhost:8080/ in the browser you should see a “Hello world!” message. If you start changing files Webpack will hot-reload the page for you.

Flux with Alt

Now it’s time to implement flux using Alt. You will need to create three new folders inside of the “flux” folder: “alt”, “stores” and “actions”:

wp-api/
— node_modules/
— src/
 — — scripts/
— — — flux/
— — — — alt/
 — — — — actions/
 — — — — stores/
 — — — components/
— — — — Home.js
 — — — index.js
 — — index.html
 — package.json
 — webpack.config.js

Create Alt.js inside of the “alt” folder and paste this in the file:

import Alt from 'alt';
const  alt = new Alt();

export default  alt;

All this file is doing is exporting the Alt instance that we will use in stores and actions.

Create DataActions.js in the “actions” folder. This file will have all the logic for getting the data from the WordPress REST API endpoints. For talking to the API we’ll use axios. Include this in DataActions.js:

import axios from 'axios';
import alt   from 'flux/alt/alt.js';

class DataActions {

    constructor() {
        const appUrl = 'http://wordpress-installation-example-url.com'; // WordPress installation url

        this.pagesEndPoint = `${appUrl}/wp-json/wp/v2/pages`; // Endpoint for getting WordPress Pages
        this.postsEndPoint = `${appUrl}/wp-json/wp/v2/posts`; // Endpoint for getting WordPress Posts
    }

    // Method for getting data from the provided end point url
    api(endPoint) {
        return new Promise((resolve, reject) => {
            axios.get(endPoint).then((response) => {
                resolve(response.data);
            }).catch((error) => {
                reject(error);
            }); 
        });     
    }

    // Method for getting Pages data
    getPages(cb){
        this.api(this.pagesEndPoint).then((response)=>{
            this.getPosts(response, cb)
        });
        return true;
    }

    // Method for getting Posts data
    getPosts(pages, cb){
        this.api(this.postsEndPoint).then((response)=>{
            const posts     = response
            const payload   = { pages, posts };

            this.getSuccess(payload); // Pass returned data to the store
            cb(payload); // This callback will be used for dynamic rout building
        });
        return true;
    }

    // This returnes an object with Pages and Posts data together
    // The Alt Store will listen for this method to fire and will store the returned data
    getSuccess(payload){
        return payload;
    }
}

export default alt.createActions(DataActions);

Don’t forget to replace the WordPress installation example URL with yours.

Create DataStore.js in “stores” folder. This file will be listening to DataActions.js’ getSuccess() method that returns data from the WordPress API. It will then store and manipulate the data. Paste this in DataStore.js:

import alt          from 'flux/alt/alt.js';
import DataActions  from 'flux/actions/DataActions.js';

class DataStore {
    constructor() {
        this.data = {};

        this.bindListeners({
            // Listen to the getSuccess() in DataActions.js
            handleSuccess: DataActions.GET_SUCCESS
        });

        this.exportPublicMethods({
            getAll:         this.getAll,
            getAllPages:    this.getAllPages,
            getAllPosts:    this.getAllPosts,
            getPageBySlug:  this.getPageBySlug
        });
    }

    // Store data returned by getSuccess() in DataActions.js
    handleSuccess(data) {
        this.setState({ data });
    }

    // Returns all pages and posts
    getAll() { 
        return this.getState().data; 
    }

    // Returns all Pages
    getAllPages() { 
        return this.getState().data.pages; 
    }

    // Returns all Posts
    getAllPosts() { 
        return this.getState().data.posts; 
    }

    // Returns a Page by provided slug
    getPageBySlug(slug){
        const pages = this.getState().data.pages;
        return pages[Object.keys(pages).find((page, i) => {
            return pages[page].slug === slug;
        })] || {};
    }

}

export default alt.createStore(DataStore, 'DataStore');

To get data from the WordPress API and make it available for the app you need to include DataActions.js in index.js and wrap the render function in DataActions.getPages(). The returned response will later be used to dynamically create routes:

import {render}             from 'react-dom';
import DataActions          from 'flux/actions/DataActions.js';
import Home                 from 'components/Home.js';

import {
    BrowserRouter as Router,
    Route,
    Redirect,
    Switch
} from 'react-router-dom';


class AppInitializer {
    run() {
        DataActions.getPages((response)=>{
            render(
                <Router>
                    <div>
                        <Switch>
                            <Route path="/" component={ Home } exact />
                            <Route render={() => { return <Redirect to="/" /> }} />
                        </Switch> 
                    </div>
                </Router>

                , document.getElementById('app')
            );
        });
    }
}

new AppInitializer().run();

Now every time the app gets initiated DataActions.getPages() calls the WordPress API endpoint and stores returned data in DataStore.js.

To access it simply include the DataStore.js in any component and call the appropriate method. For example let’s get all the data inside the Home.js file and console.log it:

import DataStore from 'flux/stores/DataStore.js'

class Home extends React.Component {
    render() {
        let allData = DataStore.getAll();
        console.log(allData); 

        return (
            <div>
                <h2>Hello world!</h2>
            </div>
        );
    }
}

export default Home;

After Webpack refreshes the page you should see the returned data object in the console:

Object {pages: Array[1], posts: Array[1]}

Dynamic Routes

Right now there are no routes set in the app other than the index route. If you have a few pages created in WordPress backend, chances are you want them to be available for the front end. To dynamically add routes to React Router we need to add another method in index.js, let’s call it buildRoutes():

import {render}             from 'react-dom';
import DataActions          from 'flux/actions/DataActions.js';
import Home                 from 'components/Home.js';

import {
    BrowserRouter as Router,
    Route,
    Redirect,
    Switch
} from 'react-router-dom';


class AppInitializer {
    buildRoutes(data){
        return data.pages.map((page, i) => {
            return(
                <Route
                    key={i}
                    component={ Home }
                    path={`/${page.slug}`}
                    exact
                /> 
            )
        })     
    }

    run() {
        DataActions.getPages((response)=>{
            render(
                <Router>
                    <div>
                        <Switch>
                            <Route path="/" component={ Home } exact />

                            {this.buildRoutes(response)}
                            <Route render={() => { return <Redirect to="/" /> }} />
                        </Switch> 
                    </div>
                </Router>

                , document.getElementById('app')
            );
        });
    }
}

new AppInitializer().run();

Include {this.buildRoutes(response)} inside of React Router right after <Route path=”/” component={ Home } exact />. The method simply loops through all pages returned by the WordPress API and returns new routes. Notice how for each route it adds the “Home” component. This means that the “Home” component will be used for every route.

Let’s say in WordPress you have a page with a slug “about. If you go to the route for that page “/about” it will load but you will still see the same “Hello World” message. In the case that you only need one template for every page, you can leave it as is and get page specific data by calling DataStore.getPageBySlug(slug) and providing the current page slug.

In most cases, though, you would need to have multiple templates for different pages.

Page Templates

In order to use page templates we need to let React know what template to use with any given page. We can use the page slug that gets returned by the API to map templates to different routes.

Let’s assume we have two pages with slugs “home” and “about”. We need to create an object that maps page slugs to React component paths. Let’s name the object templates and include it in index.js:

import {render}             from 'react-dom';
import DataActions          from 'flux/actions/DataActions.js';
import Home                 from 'components/Home.js';
import About                from 'components/About.js';

import {
    BrowserRouter as Router,
    Route,
    Redirect,
    Switch
} from 'react-router-dom';


class AppInitializer {

    templates = {
        'about': About
    }

    buildRoutes(data){
        return data.pages.map((page, i) => {
            return(
                <Route
                    key={i}
                    component={this.templates[page.slug]}
                    path={`/${page.slug}`}
                    exact
                /> 
            )
        })     
    }

    run() {
        DataActions.getPages((response)=>{
            render(
                <Router>
                    <div>
                        <Switch>
                            <Route path="/" component={ Home } exact />

                            {this.buildRoutes(response)}
                            <Route render={() => { return <Redirect to="/" /> }} />
                        </Switch> 
                    </div>
                </Router>

                , document.getElementById('app')
            );
        });
    }
}

new AppInitializer().run();

We also made updates to the buildRoutes() method to require the right component. Don’t forget to create the About.js component to map the “about” slug.

In order to get page-specific data, all you need to do is call the DataStore.getPageBySlug(slug) method and provide the current page slug:

render() {
    let page = DataStore.getPageBySlug(‘about’);
return (
        <div>
            <h1>{page.title.rendered}</h1>
        </div>
    );
}

Dynamic Navigation

Now we’re going to add a global navigation that will reflect all WordPress backend page links. First create a Header.js component in the “components” folder:

import {render}             from 'react-dom';
import DataActions          from 'flux/actions/DataActions.js';

import Home                 from 'components/Home.js';
import About                from 'components/About.js';
import Header               from 'components/Header.js';

import {
    BrowserRouter as Router,
    Route,
    Redirect,
    Switch
} from 'react-router-dom';


class AppInitializer {

    templates = {
        'about': About
    }

    buildRoutes(data){
        return data.pages.map((page, i) => {
            return(
                <Route
                    key={i}
                    component={this.templates[page.slug]}
                    path={`/${page.slug}`}
                    exact
                /> 
            )
        })     
    }

    run() {
        DataActions.getPages((response)=>{
            render(
                <Router>
                    <div>
                        <Header />

                        <Switch>
                            <Route path="/" component={ Home } exact />

                            {this.buildRoutes(response)}
                            <Route render={() => { return <Redirect to="/" /> }} />
                        </Switch> 
                    </div>
                </Router>

                , document.getElementById('app')
            );
        });
    }
}

new AppInitializer().run();

We get all pages from WordPress using DataStore.getAllPages(), then we’re sorting them by “menu_order” with lodash and looping through them to spit out the React Router’s <Link />. Note that the homepage route is being excluded from the allPages array and included as a separate link.

Include the Header.js component into index.js and you’ll see the dynamic nav included on every page:

import {Link} from 'react-router-dom';
import DataStore from 'flux/stores/DataStore.js'

class Header extends React.Component {   
   
    render() {
        let allPages = DataStore.getAllPages();
        allPages = _.sortBy(allPages, [function(page) { return page.menu_order; }]); // Sort pages by order

        return (
            <div className="header">
                <Link to="/" style={{marginRight: '10px'}} >Home</Link>

                {allPages.map((page) => {
                    if(page.slug != 'home'){
                       return(
                            <Link 
                                key={page.id} 
                                to={`/${page.slug}`} 
                                style={{marginRight: '10px'}}
                            >
                                {page.title.rendered}
                            </Link>
                        )                     
                   }
                })}
            </div>
        );
    }
}

export default Header;

Advanced Custom Fields

Most WordPress developers are familiar with Advanced Custom Fields plugin. It makes the WordPress CMS fully customizable and user friendly. Fortunately, it’s very easy to access ACF data by utilizing WordPress API.

To get ACF data from API endpoints we need to install another plugin called ACF to REST API. This will include an acf property in the object returned by the WordPress API. You can access the acf fields like so:

render() {
    let page = DataStore.getPageBySlug(‘about’);
    let acf = page.acf; // Advanced Custom Fields data
return (
        <div>
            <h1>{acf.yourCustomFieldName}</h1>
        </div>
    );
}

Next Steps

All right, we’ve covered the most common use case for leveraging the WordPress CMS admin, along with a React front-end.

Some next steps might be to add styling for the project in Less or Sass. Or maybe extending the DataAction.js file by adding additional API endpoint calls to pull more data like comments, categories, and taxonomies.

I strongly recommend checking out the official WordPress REST API handbook, where the API functionality is well documented. There you’ll find information about CRUD, pagination, authentication, querying, creating custom endpoints, and more. These resources will help extend what we’ve built here.