Step-by-step Guide to displaying WordPress Rest API posts in a React WordPress theme

Part 2 in a series on Creating a React WordPress theme, how to display WordPress Rest API Posts listed with infinite scroll to paginate, along with single post view.

This post is part of the React WordPress Theme Collection

An Step-by-step guide on creating a React WordPress theme, to list WordPress Rest API posts and pages, display and create comments, integrate Server Side rendering using PHP.

  1. Create a React WordPress theme using babel and webpack
  2. Step-by-step Guide to displaying WordPress Rest API posts in a React WordPress theme
  3. Guide to Creating React WordPress menus in a WordPress Theme
  4. Using WordPress Permalinks in a Custom WordPress React Theme

This article continues on from the previous article where we setup a WordPress React Theme using Babel and Webpack, we will begin with listing and viewing WordPress Rest API posts.

  1. Create a React Component to display a list WordPress Rest API posts.
  2. Add pagination using infinite scrolling pattern.
  3. Use React Router to navigate between archive and single posts.
  4. Display a single WordPress Rest API post in a React Component.

We will be using the Axios http client library to fetch data from the WordPress Rest API, and also we will be using lodash debounce module when we are integrating infinite scrolling.

All packages used in this article can be installed using one of the following commands, depending which package manager you are using Yarn or NPM.

# 1. Move into the theme folder
cd /var/www/html/wp-content/themes/jclabs-react-theme/

# 2. Yarn
yarn add lodash.debounce axios

# 2. NPM
npm i --save lodash.debounce axios

Once the packages have been installed, use the following command to run webpack to watch and build our javascript files, along with auto reloading the development server using browser-sync that can be accesed via http://localhost:3000.

# 2. Yarn
yarn start

# 2. NPM
npm run start

Displaying a list of WordPress Rest API posts

An overview of the themes files and components that we will be adding/updating when displaying WordPress Rest API posts.

/jclabs-react-theme/
|----/src/
|    |----/Archive/
|    |    |- Archive.js
|    |----/Post/
|    |    |- Post.js
|    |- App.js
|- functions.php

Adding WordPress Rest Url to Javascript

Lets start by updating the function jcrt_enqueue_scripts from the themes functions file, adding rest_url to the config array so that the react application will be able to access it via window.wp_config

functions.php

function jcrt_enqueue_scripts()
{
    $version = '1.0.0';
    if (defined('WP_DEBUG') && WP_DEBUG === true) {
        $version = time();
    }

    wp_enqueue_script('theme', get_stylesheet_directory_uri() . '/dist/js/bundle.js', array('jquery'), $version, true);
    wp_enqueue_style('theme', get_stylesheet_directory_uri() . '/dist/css/style.bundle.css', array(), $version);

    $config = array(
        // TODO: Add any theme variables needed in react
        'rest_url' => rest_url(),
    );
    wp_localize_script('theme', 'wp_config', $config);
}
add_action('wp_enqueue_scripts', 'jcrt_enqueue_scripts');

Archive Component

The Archive component will fetch a list of WordPress Rest API posts and store these in its state, once loaded it will render each item of this list using the new Post component that we will create next.

We start by setting the initial state in the class constructor

constructor() {
  this.state = {
    posts: [],
    loaded: false,
    error: ''
  };
}

Then when the Archive component is mounted we fetch the list of posts from the WordPress API using wp_config.rest_url variable that we added previously via the functions file, the new posts that are fetched are appended onto the current list of posts using javascripts concat function.

componentDidMount() {
  Axios.get(
    window.wp_config.rest_url + 'wp/v2/posts'
  ).then(
    response => {
      const { posts } = this.state;
      this.setState({
        posts: posts.concat(response.data),
      });
    }
  );
}

We then check to see if the component state has posts, and if so we loop through the posts setting the key as the id of the post as this is unique and passing the data to the Post component.

render() {
  const { posts} = this.state;
  return (
    <div className="archive">
      {posts && (
        <div
          className="archive__body"
        >
          {posts.map(post => (
            <Post key={post.id} post={post} />
          ))}
        </div>
      )}
    </div>
  );
}

Putting this all together and adding loading and error states.

src/Archive/Archive.js

Create the new Archive Component with the following code at src/Archive/Archive.js.

import React, { Component } from 'react';
import Axios from 'axios';
import Post from '../Post/Post';

class Archive extends Component {
  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      loaded: false,
      error: ''
    };

    this.getPosts = this.getPosts.bind(this);
  }

  getPosts() {
    this.setState({ loaded: false });

    return Axios.get(
      window.wp_config.rest_url + 'wp/v2/posts'
    ).then(
      response => {
        const { posts} = this.state;
        this.setState({
          posts: posts.concat(response.data),
          loaded: true
        });
      },
      error => {
        this.setState({
          error: error.toJSON().message,
          loaded: true
        });
      }
    );
  }

  componentDidMount() {
    this.getPosts();
  }

  render() {
    const { posts, loaded, error} = this.state;
    return (
      <div className="archive">
        <div className="archive__header">
          <h1 className="archive__heading">Recent Posts</h1>
        </div>
        {error && (
          <div className="notice notice--error">
            <p>Unable to load posts due to the error: {error}</p>
          </div>
        )}
        {posts && (
          <div
            className="archive__body"
          >
            {posts.map(post => (
              <Post key={post.id} post={post} />
            ))}
          </div>
        )}
        {!loaded && (
          <div className="notice notice--loading">
            <p>Loading.</p>
          </div>
        )}
      </div>
    );
  }
}

export default Archive;

Post Component

The post component takes the post prop that is passed from the archive component and displays the posts details, because the WordPress Rest API renders the excerpt into html, this cant just be output using curly braces {variable} like we have done for the post title, instead we have to use the dangerouslySetInnerHTML attribute to render it.

src/Post/Post.js

Create the new Post Component with the following code at src/Post/Post.js.

import React, { Component } from 'react';

class Post extends Component {
  render() {
    const { post } = this.props;
    return (
      <article className="post">
        <header className="post__header">
          <h1>{post.title.rendered}</h1>
        </header>
        <section
          className="post__excerpt"
          dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
        />
      </article>
    );
  }
}

export default Post;

App Component

Lets update the existing app component removing the Hello World text and display the new Archive Component.

src/App.js

Replace the App Component with the following code

import React, { Component } from 'react';
import Archive from './Archive/Archive';

class App extends Component {
  render() {
    return (
      <div className="site-wrapper">
        <Archive />
      </div>
    );
  }
}

export default App;

If you have been following allong, if you view your WordPress website you should now see a basic list of posts being displayed on the screen.

Paginate WordPress posts using infinite scroll

Now that we have a list of posts displayed, lets introduce some pagination with a sprinkling of infinite scrolling. To do this we need to add the page number into the WordPress Rest API request this is done by simply passing page parameter with the page number.

Axios.get(
  window.wp_config.rest_url + 'wp/v2/posts?page=' + page
)

Next we want to create a html reference to the container that is wrapping around our list of posts, this is so we can add its height into the load more calculation.

<div
  className="archive__body"
  ref={scrollBox => (this.scrollBox = scrollBox)}
>

Within our Archive Component we want to add an event lister to trigger when the user scrolls, so we bind this to the window. Always make sure that if you bind events to unbind them using Reacts componentWillUnmount lifecycle hook.

componentDidMount() {
  window.addEventListener('scroll', this.onScroll, false);
}

componentWillUnmount() {
  window.removeEventListener('scroll', this.onScroll, false);
}

The function that we have bound to the scroll event check to see how far down the page the current user is scrolled, if they are less than the height of the window away from the end of the list of posts, it will trigger the request to load the next page of posts, and repeating this until there are no more pages.

onScroll() {
  const node = this.scrollBox;
  if (node) {
    window.requestAnimationFrame(() => {
      const scrollPos = window.scrollY;
      const windowHeight = window.innerHeight;
      const nodeOffset = node.offsetTop;
      const nodeHeight = node.offsetHeight;

      /**
       * Trigger loadMore if less than a screen hight
       * from end of archive
       */
      if (
        scrollPos - nodeOffset + windowHeight > nodeHeight - windowHeight &&
        this.state.loaded === true &&
        this.state.hasMore
      ) {
        this.loadMore();
      }
    });
  }
}

With all this in mind i have updated the Archive component with the previously mentioned infinite scroll pagination.

Archive/Archive.js

Replace the existing Archive component with the updated code.

import React, { Component } from 'react';
import Axios from 'axios';
import Post from '../Post/Post';
import debounce from 'lodash.debounce';

class Archive extends Component {
  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      loaded: false,
      error: '',
      hasMore: true,
      page: 1
    };

    this.getPosts = this.getPosts.bind(this);
    this.loadMore = this.loadMore.bind(this);
    this.onScroll = debounce(this.onScroll.bind(this), 100);
  }

  getPosts() {
    const { page } = this.state;
    this.setState({ loaded: false });

    return Axios.get(
      window.wp_config.rest_url + 'wp/v2/posts?page=' + page
    ).then(
      response => {
        const { posts, page } = this.state;
        this.setState({
          posts: posts.concat(response.data),
          loaded: true,
          page: page + 1
        });
      },
      error => {
        this.setState({
          error: error.toJSON().message,
          loaded: true,
          hasMore: false
        });
      }
    );
  }

  componentDidMount() {
    this.loadMore();

    window.addEventListener('scroll', this.onScroll, false);
  }

  onScroll() {
    const node = this.scrollBox;
    if (node) {
      window.requestAnimationFrame(() => {
        const scrollPos = window.scrollY;
        const windowHeight = window.innerHeight;
        const nodeOffset = node.offsetTop;
        const nodeHeight = node.offsetHeight;

        /**
         * Trigger loadMore if less than a screen hight
         * from end of archive
         */
        if (
          scrollPos - nodeOffset + windowHeight > nodeHeight - windowHeight &&
          this.state.loaded === true &&
          this.state.hasMore
        ) {
          this.loadMore();
        }
      });
    }
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.onScroll, false);
  }

  loadMore() {
    this.getPosts().then(() => {
      /**
       * Check to see if content loaded is
       * greater than the height of the screen,
       * if not load next page of data.
       */
      const node = this.scrollBox;
      if (window.innerHeight > node.offsetTop + node.offsetHeight) {
        this.loadMore();
      }
    });
  }

  render() {
    const { posts, loaded, error, page } = this.state;
    return (
      <div className="archive">
        <div className="archive__header">
          <h1 className="archive__heading">Recent Posts</h1>
        </div>
        {error && page === 0 && (
          <div className="notice notice--error">
            <p>Unable to load posts due to the error: {error}</p>
          </div>
        )}
        {posts && (
          <div
            className="archive__body"
            ref={scrollBox => (this.scrollBox = scrollBox)}
          >
            {posts.map(post => (
              <Post key={post.id} post={post} />
            ))}
          </div>
        )}
        {!loaded && (
          <div className="notice notice--loading">
            <p>Loading.</p>
          </div>
        )}
      </div>
    );
  }
}

export default Archive;

Navigate between post archive and single page using React Router

To allow the user to view single posts we need to introduce the React Router Dom and update the App Component to use a Switch to load the correct route depending on the current url.

For this to work we need to enable Permalinks using the post name setting, and update our App Component with the code below.

App.js

Replace the App Component with the following code.

import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import Archive from './Archive/Archive';
import Single from './Single/Single';

class App extends Component {
  render() {
    // Requires that permalink structure is set to Post name
    return (
      <div className="site-wrapper">
        <Switch>
          <Route path="/:post">
            <Single />
          </Route>
          <Route path="/" exact={true}>
            <Archive />
          </Route>
        </Switch>
      </div>
    );
  }
}

export default App;

As you can see in the previous code we have declared the “/” route to load the Archive component, and using a route match we are going to capture the post slug and show the Single component that we will create in the next section.

To capture the post slug from the url we need to import and use the withRouter higher order component to access information about the current route.

import { withRouter} from 'react-router-dom';
// ...
export default withRouter(Single);

Accessing React Router’s matched route can be done through the Single components props as shown below.

componentDidMount() {
  const { post } = this.props.match.params;
}

Display a singular WordPress Rest API post

With the Routing setup, we can now create the Single Component that will fetch the chosen post by slug from the WordPress Rest API, and pass the post detail onto the PostSingle Component.

/jclabs-react-theme/
|----/src/
|    |----/Single/
|    |    |- Single.js
|    |----/PostSingle/
|    |    |- PostSingle.js
|    |----/Post/
|    |    |- Post.js

Single Component

The single component is very similar to the Archive component, we start by setting the initial state in the class constructor.

constructor() {
  this.state = {
    post: {},
    loaded: false,
    error: ''
  };
}

When the component is mounted we read the post slug from React Router, and fetch the post via the WordPress Rest API adding it to our components state.

componentDidMount() {
  const { post } = this.props.match.params;

  Axios.get(window.wp_config.rest_url + 'wp/v2/posts?slug=' + post).then(
    response => {
      this.setState({
        post: response.data[0],
      });
    }
  );
}

With the post data fetched we now render the SinglePostComponent

render() {
  const { post } = this.state;
  return (
    <div className="single">
      {post && <PostSingle post={post} />}
    </div>
  );
}

With all that in mind lets create the Single component, adding loading and error states.

src/Single/Single.js

Create the new Single Component at src/Single/Single.js with the following code.

import React, { Component } from 'react';
import { withRouter, Link } from 'react-router-dom';
import Axios from 'axios';
import PostSingle from '../PostSingle/PostSingle';

class Single extends Component {
  constructor(props) {
    super(props);

    this.state = {
      post: {},
      loaded: false,
      error: ''
    };

    this.getPost = this.getPost.bind(this);
  }

  getPost(slug) {
    this.setState({ loaded: false });
    Axios.get(window.wp_config.rest_url + 'wp/v2/posts?slug=' + slug).then(
      response => {
        this.setState({
          post: response.data[0],
          loaded: true
        });
      },
      error => {
        this.setState({
          error: error.toJSON().message,
          loaded: true
        });
      }
    );
  }

  componentDidMount() {
    const { post } = this.props.match.params;
    this.getPost(post);
  }

  render() {
    const { post, loaded, error } = this.state;
    return (
      <div className="single">
        <Link to="/" className="back">
          < Back
        </Link>
        {error && page === 0 && (
          <div className="notice notice--error">
            <p>Unable to load post due to the error: {error}</p>
          </div>
        )}
        {!loaded && (
          <div className="notice notice--loading">
            <p>Loading.</p>
          </div>
        )}
        {error && <p>Unable to load posts due to the error: {error}</p>}
        {loaded && post && <PostSingle post={post} />}
      </div>
    );
  }
}

export default withRouter(Single);

PostSingle Component

The PostSingle component renders the post that is passed to it, similarly to the Post Component we use Reacts dangerouslySetInnerHTML attribute to display the post content that WordPress renders.

PostSingle/PostSingle.js

Create and add the following code to the PostSingle Component at src/PostSingle/PostSingle.js

import React, { Component } from 'react';

class PostSingle extends Component {
  render() {
    const { post } = this.props;
    return (
      <article className="post">
        <header className="post__header">
          <h1>{post.title.rendered}</h1>
        </header>
        <section
          className="post__content"
          dangerouslySetInnerHTML={{ __html: post.content.rendered }}
        />
      </article>
    );
  }
}

export default PostSingle;

Post Component

We need update the Post Component that displays each indivual post on the post archive and using the Link Component from React Router we can link the Post Archive List to the Single view, because the link is coming from the WordPress Rest API, using the method parseLink to alter the link removing the site url so that it starts from the website route.

src/Post/Post.js

Update the Post Component with the following code at src/Post/Post.js

import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class Post extends Component {
  parseLink(link) {
    if (typeof link === 'undefined') {
      return '/';
    }

    if (!link.match(/https?:\/\//)) {
      return link;
    }
    const pos = link.indexOf('/', 8);
    return link.substr(pos);
  }

  render() {
    const { post } = this.props;
    return (
      <article className="post">
        <header className="post__header">
          <h1>{post.title.rendered}</h1>
        </header>
        <section
          className="post__excerpt"
          dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
        />
        <Link to={this.parseLink(post.link)} className="btn">
          View Post
        </Link>
      </article>
    );
  }
}

export default Post;

With the last component created we should now have a React WordPress Theme that allows you to view a paginated list of posts by auto fetching the next page of posts before you reach the bottom using infinite scrolling technique.

This post is part of the React WordPress Theme Collection

An Step-by-step guide on creating a React WordPress theme, to list WordPress Rest API posts and pages, display and create comments, integrate Server Side rendering using PHP.

  1. Create a React WordPress theme using babel and webpack
  2. Step-by-step Guide to displaying WordPress Rest API posts in a React WordPress theme
  3. Guide to Creating React WordPress menus in a WordPress Theme
  4. Using WordPress Permalinks in a Custom WordPress React Theme

Leave a Reply

Fields marked with an * are required to post a comment