In this tutorial series, we’ve been demonstrating how you can build a blog engine with Carbon LDP™ and React. Carbon serves our data layer and React is the library we’re using for the user interface.

In part 1, we set up the front-end with React and we created a simple blog post input form. In part 2, we updated the form so that when it’s submitted, blog post data is saved in Carbon. Today, in part 3, we’ll implement the blog post list and detail view so that users can browse and read the posts from the Carbon server. Let’s get started.

Note: Complimentary source code for this tutorial can be found in the GitHub repo, carbonldp-react-blog.

Stub out an example list on the Blog view

For the list of recent blog posts, we already have a view component that you can navigate to by clicking Blog in the app’s menu. Let’s first rough out what we want that view to look like with some Material-UI components and some mock data. We’ll swing back around later to replace the mock data with real data retrieved from Carbon.

Initially, our code will simply demonstrate how to iterate over a dataset in order to create our JSX. If you already know how to do this in React, you might skip ahead. Just for demonstration purposes, we’ll start by fetching some data from the SWAPI Star Wars API

Modify src/components/blog/index.js to be as follows:

import React, { Component } from 'react';
class Blog extends Component {

  constructor(props) {
    super(props);
    this.state = {items: []}
  }

  componentWillMount() {
    fetch( 'https://swapi.co/api/people/?format=json' )
        .then( response => response.json() )
        .then( ({results: items}) => this.setState({items}) )
  }

  render() {
    let items = this.state.items;
    return (
      <div className="Blog">
        <h1>Blog</h1>
          {items.map(item => <h4>{item.name}</h4>)}
      </div>
    );
  }
}

export default Blog;

This will render a list of characters from Star Wars.

Blog list stub with Star Wars test data

You will notice, however, that the browser console is giving the following warning:

Warning: Each child in an array or iterator should have a unique "key" prop.

Browser console error

That is telling us that our JSX element needs a key that is equal to something unique. The items from the Star Wars API do not have an id, so we’ll use item.name for the key. We can fix this by adding the key to the <h4/> element like this:

<h4 key={item.name}>{item.name}</h4>

Now, with the key in place, the console error goes away. Having a unique key for dynamic JSX elements is a requirement in React.

It’s worth showing here that there’s also another way to do this, which you may prefer. We can create a separate component for the iteration. To see this alternative approach in action, replace the contents of the file with the following:

import React, { Component } from 'react';

const Person = (props) => <h4>{props.person.name}</h4>;

class Blog extends Component {

  constructor(props) {
    super(props);
    this.state = {items: []}
  }

  componentWillMount() {
    fetch( 'https://swapi.co/api/people/?format=json' )
        .then( response => response.json() )
        .then( ({results: items}) => this.setState({items}) )
  }

  render() {
    let items = this.state.items;
    return (
      <div className="Blog">
        <h1>Blog</h1>
          {items.map(item => <Person key={item.name} person={item}/> )}
      </div>
    );
  }
}

export default Blog;

Retrieve blog post list from Carbon

Now, we’ll use a technique for Reading documents, which is covered in the Carbon LDP™ product documentation. It is fairly common for applications to require only certain properties of a document. This can happen, for example, when rendering a list of objects and just displaying their name. In our case, we only need to render certain properties of the blog posts in our list, so we’ll use this approach. Here’s how.

In src/components/blog/index.js, let’s import and configure the Carbon object as follows:

import {CarbonLDP} from "carbonldp/CarbonLDP";

let carbonldp = new CarbonLDP("http://localhost:8083");

Next, we’ll change the following line:

const Person = (props) => <h4>{props.person.name}</h4>;

Change it to:

const Post = (props) => 
    <div><h4><Link to={`${props.post.slug}/`}>{props.post.title}</Link></h4><p>{props.post.excerpt}</p></div>;

And then we’ll change the following line:

{items.map(item => <Person key={item.name} person={item}/> )}

Change it to:

{items.map(item => <Post key={item.$id} post={item}/> )}

We can then remove the fetch (Star Wars API) stanza and replace it with code to get data from Carbon. We will do this in the componentWillMount() function like so:

componentWillMount() {
      carbonldp.documents.$getMembers( "posts/", _ => _
          .properties( {
              "title": {
                  "@type": "string"
              },
              "slug": {
                  "@type": "string"
              },
              "excerpt": {
                  "@type": "string"
              }
          } )
      ).then(  (items) => {
              console.log('Items: %o', items);
              this.setState({items});
          }
      );
  }

The entire file, src/components/blog/index.js, should now be as follows:

import React, { Component } from 'react';
import { Link } from "react-router-dom";
import {CarbonLDP} from "carbonldp/CarbonLDP";

let carbonldp = new CarbonLDP("http://localhost:8083");

const Post = (props) => 
    <div><h4><Link to={`${props.post.slug}/`}>{props.post.title}</Link></h4><p>{props.post.excerpt}</p></div>;

class Blog extends Component {

  constructor(props) {
    super(props);
    this.state = {items: []}
  }

  componentWillMount() {
      carbonldp.documents.$getMembers( "posts/", _ => _
          .properties( {
              "title": {
                  "@type": "string"
              },
              "slug": {
                  "@type": "string"
              },
              "excerpt": {
                  "@type": "string"
              }
          } )
      ).then(  (items) => {
              this.setState({items});
          }
      );
  }

  render() {
    let items = this.state.items;
    return (
      <div className="Blog">
        <h1>Blog</h1>
          {items.map(item => <Post key={item.$id} post={item}/> )}
      </div>
    );
  }
}

export default Blog;

Now, when we run the app using npm start, we are able to click on the Blog menu item and see the list of posts that have been retrieved from Carbon.

The blog post list view

In keeping with our practice of committing at stable checkpoints, I will commit here with the following message, so that you can find this checkpoint in the Git history.

Commit: Retrieve blog post list from Carbon

Stub blog detail view with parameterized route

Next, we need a component for rendering the actual blog post when we click on a single blog post from the list. We’ll call this the BlogDetail component.

Create the directory and file, src/components/blog-detail/index.js, with the following contents:

import React, { Component } from 'react';

class BlogDetail extends Component {

    componentDidMount() {
        const { slug } = this.props.match.params;
        console.log('Slug: %o',slug)
    }
    render() {

        return (
            <div className="Home">
                <h1>Blog Detail</h1>
            </div>
        );
    }
}

export default BlogDetail;

In src/App.js, import the newly created BlogDetail component with the following:

import BlogDetail from './components/blog-detail';

Also in src/App.js, add the following new parameterized route before the closing </Switch> tag:

<Route path="/:slug" component={BlogDetail}/>

If you run the app now (using npm start), you can enter any string for the second path segment of the URL to load the Blog Detail view. The route parameter (slug) will be logged to the console. Try this now with any URL such as the following:

http://localhost:3000/hello-world

The console should log the following statement:

Slug: "hello-world"

As you can see, we’ve got a handle on the parameter that’s passed through the parametrized route, which we’ll soon use to find the appropriate document to load from Carbon.

Parameterized route logged as slug

We’ll commit here at this stable checkpoint with the following Git message:

Commit: Stub blog detail view with parameterized route

Load blog post from Carbon by slug

Next, we’re going to use the slug passed as a parameter to the route to identify the document to load from Carbon. This will be done in src/blog-detail/index.js with code in the React componentDidMount lifecycle method…

componentDidMount() {
    const {slug} = this.props.match.params;
    carbonldp.documents.$get("posts/" + slug + "/")
        .then((persistedPost) => {
            this.setState({post: persistedPost});
        })
        .catch(error => console.error(error));
}

Here, we’re simply using the slug from the route to call for the document in Carbon under posts/, which we know we named by slug. Notice that we appended a trailing forward-slash, however. This is something that people often forget when trying to resolve documents in Carbon. The forward-slash is part of the document’s id and therefore necessary to locate it. Unlike on some web servers, Carbon does not treat the lack or addition of a forward-slash as the same.

Update src/components/blog-detail/index.js to have the following contents:

import React, {Component} from 'react';

import {CarbonLDP} from "carbonldp/CarbonLDP";

let carbonldp = new CarbonLDP("http://localhost:8083");

class BlogDetail extends Component {

    constructor(props) {
        super(props);
        this.state = {
            post: {
                $id: '',
                title: '',
                slug: '',
                excerpt: '',
                body: ''
            }
        };
    }

    componentDidMount() {
        const {slug} = this.props.match.params;
        carbonldp.documents.$get("posts/" + slug + "/")
            .then((persistedPost) => {
                this.setState({post: persistedPost});
            })
            .catch(error => console.error(error));
    }

    render() {
        let post = this.state.post;
        return (
            <div className="Home">
                <h1>Blog Detail</h1>
                <h2>{post.title}</h2>
                <p>ID: {post.$id}</p>
                <p>Slug: {post.slug}</p>
                <p>Excerpt: {post.excerpt}</p>
                <hr/>
                {post.body}
            </div>
        );
    }
}

export default BlogDetail;

Now, if you run the app (with npm start), you will be able to manually enter any known slugs into the URL and navigate to the blog detail view, which looks like this:

The blog post detail view

Actually, we can also click on the blog titles in the blog list to navigate to the detail view. That is because, in src/components/blog/index.js, we defined the constant Post as follows:

const Post = (props) => 
    <div><h4><Link to={`${props.post.slug}/`}>{props.post.title}</Link></h4><p>{props.post.excerpt}</p></div>;

Here, we’re using the React Link component to create a hyperlink with the slug of the particular blog post being iterated over in the view.

I’ll commit at this stable checkpoint with the following Git commit message so that you can find it in the commit history.

Commit: Implement load blog post from Carbon by slug

Wrap up

In this tutorial series we’ve demonstrated how to build a simple blog engine with Carbon and React. Today, we implemented the blog post list and detail view so that users can browse and read the posts from the Carbon server. We:

  • Stubbed out an example list on the Blog view,
  • Retrieved blog post list from Carbon,
  • Stubbed blog detail view with parameterized route,
  • Loaded blog post from Carbon by slug, and
  • Linked blog list items to the BlogDetail view.

Our blog engine is undoubtedly simple but it forms the foundation of something you could now enhance and extend. In this series, we’ve demonstrated some very basic React development and how to read and write data from and to the Carbon server. Hopefully, this has illustrated how simple it is to use Carbon as the backend for a data-centric web app. Whether you’re using React, Angular, or any other popular front-end framework, Carbon LDP™ reduces your need to wrestle with server-side plumbing. Once you spin up the Carbon server, you’re ready to read and write data with the help of Carbon’s JavaScript SDK.