React - Intro, Setup, and GET all


April 2017 update - note that the dependencies for this course may now be out of date (hey, the JS ecosystem moves quickly!). If unsure, please use the versions from package.json in the code repository for this part of the course.

In this section we are going to re-implement the CRUD setup, but this time we will use React, along with a few extra modules, to interact with our API.

Again, much like the Angular section, I want to start by saying that this is not intended as a beginners guide to React. This is intended as a possible way of interacting with a Symfony 3 API from a React project. It is not the defacto way, and very possibly it is not a best practice way either. So, as ever, use your own judgement on how relevant this is to you / your project.

One other thing to note before we get started - I am basing the setup of this project of Gaearon's React Hot Boilerplate. There are some points to note about this.

If you are unaware, Gaearon is the online alias of Dan Abramov, co-creator of Redux, React Hot Loader (as used in this project), React Drag n Drop, and likely a whole lot more. Now, controversially, using a boilerplate / starter project goes largely against Dan Abramov's opinions as of late.

However, as mentioned this is not a project aiming to show you how to use React, and this was also one of the most minimal boilerplates I could find. I want to focus on the connectivity section, rather than the environment setup, so this is my reasoning.

This starter project is approx 1 year old at the time of writing. This project uses React 0.14.6. At the time of writing, React is at v15.2.1! Note that React jumped from 0.14.x to 15.x, so this is not quite as out-of-date as it initially looks.

Additional Libraries

We will need a router, and a library for handling our HTTP interactions.

For the router, I am going to use React Router which is going to help keep our various components in sync with what is displayed in the URL. Don't worry too much about this, this is all explained in a future video. We won't need it to begin with anyway.

For talking to our Symfony 3 API we will use the Isomorphic Fetch library, which is an incredibly technobabbly name, but is pretty straightforward conceptually. There's a likely future standard for HTTP interaction available to us today in modern browsers (and back-compatible using polyfills) called Fetch. This library wraps Fetch and makes it available for both front-end (e.g. React) and back-end (e.g. Node JS) code. That's all isomorphic means - front and back end in one library.

Everything else will be pre-setup for us by the starter project, but if you'd like to see exactly what version are in use, check out the package.json file.

Getting Started

Being based on a starter project means most of the heavy lifting of getting started is done for us. Here's the abbreviated version:

cd ~
mkdir react-symfony-3.dev
git clone git@github.com:gaearon/react-hot-boilerplate.git .
npm install
npm install --save isomorphic-fetch@2.2.1 react-router@2.5.2
npm start

Then - as it will prompt you - open up http://localhost:3000 in your browser, and you should be good to go.

One important part to note is that you must update your Symfony 3 config to enable connectivity:

# /app/config/parameters.yml

    # nelmio cors
    cors_allow_origin: 'http://localhost:3000'

Unlike the Angular starter project however, this is super bare-bones, so we have quite a bit of work ahead.

Proof Of Concept

As in the first Angular video, in this first video on React all we will do is pull in the 50 blog posts and display them. We won't concern ourselves with the project's structure, only in proving that connectivity is established, as simply as possible.

With this in mind, the first thing I am going to do is copy/paste the table from our Twig project, and paste it right into the App.js file:

// /src/App.js

import React, { Component } from 'react';

export default class App extends Component {

  render() {
    return (
        <div>
            <h1>Hello, World!</h1>

            <table class="table table-hover table-responsive">
                <thead>
                    <tr>
                        <th>id</th>
                        <th>Title</th>
                        <th>Options</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td>{ post.id }</td>
                        <td>{ post.title }</td>
                        <td>
                            <a href="#" class="btn btn-default btn-sm">Edit</a>
                            <a href="#" class="btn btn-danger btn-sm">Delete</a>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    );
  }
}

I've had to tidy up the the for ... in loop to be more JavaScript friendly, but even so, this won't actually render currently as { post.id } and { post.title } are referencing post, which doesn't exist.

We could remove the brackets:

<td>post.id</td>
<td>post.title</td>

And it should render now, but it's not a very interesting thing to prove as it's fairly obvious React works... just ask Facebook :)

Getting Our Blog Posts

Unlike in our Angular project, there is not 'controller' as such where we can stick a function that just happens to invoke itself as soon as our page loads.

Instead, we need to rely on a lifecycle method available in React's component. In a nutshell, React's Lifecycle Methods are functions which are called by React itself during various stages of the component's life. For example, when the component is first rendered in the client's browser, React will call the component's componentDidMount method for you, which can be handy for setting up the component as we shall soon see. Read the docs for more if this is still confusing, or leave a comment and I will try explain it in a different way.

Incidentally - function vs method - they are the same thing, with a different context. Methods are functions that exist inside the scope of a JavaScript object, whereas functions can exist outside other objects... :) But functionally (excuse the pun) they are indentical.

Anyway, getting back to it, knowing this about React's lifecycle methods, we can add our own componentDidMount method to our App class and use this to invoke a fetch call, which will retrieve our blog posts via our API:

// /src/App.js

import React, { Component } from 'react';
import fetch from 'isomorphic-fetch'

export default class App extends Component {

  componentDidMount() {

    fetch('http://api.symfony-3.dev/app_dev.php/posts', {
      method: 'GET',
      mode: 'CORS'
    }).then(res => res.json())
    .then(data => {
      console.log('json data', data)
    }).catch(err => err);

  }

  render() {
    return (
        <div>
            <h1>Hello, World!</h1>

            <table class="table table-hover table-responsive">
                <thead>
                    <tr>
                        <th>id</th>
                        <th>Title</th>
                        <th>Options</th>
                    </tr>
                </thead>
                <tbody>

                </tbody>
            </table>
        </div>
    );
  }
}

I have removed the tbody content just so we get something rendering. We will fix that momentarily.

On refresh now, you should be able to see your 50 blog posts inside your console.log output. Cool.

Why must we specify mode: 'CORS'? Good question. Because browsers tend to do their own thing. Being explicit here makes sures all browsers behave as (much as can be) expected. There's more on this here if interested.

Displaying Data

Right now, as soon as the fetch method resolves we are logging out some data to the console, and then disgarding the data. That's not so good. We'd definitely like to keep the current state of our blog posts around, so we can render them out to the screen.

  componentDidMount() {

    fetch('http://api.symfony-3.dev/app_dev.php/posts', {
      method: 'GET',
      mode: 'CORS'
    }).then(res => res.json())
    .then(data => {
      this.setState({
        blogPosts: data
      })
    }).catch(err => err);

  }

Again, this is a prototype of our app's functionality. We will sort this out in the next few videos. But for now, we will simply keep track of our blog posts by calling the setState method, available to us as we are extending React's Component.

Assuming our API is returning our blog posts as expected - verifiable by the console log from earlier - then the next step is to quickly render them out, bringing our proof-of-concept implementation to conclusion.

    render() {
        return (
            <div>
                <table className="table table-hover table-responsive">
                    <thead>
                    <tr>
                        <th>id</th>
                        <th>Title</th>
                        <th>Options</th>
                    </tr>
                    </thead>
                    <tbody>

                    {this.state.blogPosts && this.state.blogPosts.map(post => {
                        return (
                            <tr key={post.id}>
                                <td>{post.id}</td>
                                <td>{post.title}</td>
                                <td>
                                    <a href="#" className="btn btn-default btn-sm">Edit</a>
                                    <a href="#" className="btn btn-danger btn-sm">Delete</a>
                                </td>
                            </tr>
                        );
                    })}
                    </tbody>
                </table>
            </div>
        );
    }

Note, all the class attributes have been updated to className - this is something you have to watch out for when using React. You may notice errors about this in your browsers console log:

Warning: Unknown DOM property class. Did you mean className?

We will see something similar a little later when using the form's <label for=""> attribute.

Here, we firstly check if the blogPosts property exists in our state. If this returns false then we stop here and don't do anything further with this blog of code.

At this stage this would return false on first render, as we have not set up any initial state. Let's fix that:

// /src/App.js

import React, { Component } from 'react';
import fetch from 'isomorphic-fetch'

export default class App extends Component {

    constructor(props) {
        super(props);

        this.state = { 
            blogPosts: []
        };
    }

With our initial state set up, the initial check :

{this.state.blogPosts && ...

Will now be returning true.

In the next part of this statement we map a function onto each blogPost in the state. This simply means we take a function that can be applied to one single element / blog post, and then loop through and run that function against every blog post in this.state.blogPosts.

The first thing to notice is we are using post.id as the key, which helps React figure out which blog post is which, should we do any shuffling or sorting of our data. Note that this is different to the video. I believe this method is preferable.

All this function does is return a piece of HTML for each of the blog posts in our state:

return (
    <tr key={post.id}>
        <td>{post.id}</td>
        <td>{post.title}</td>
        <td>
            <a href="#" className="btn btn-default btn-sm">Edit</a>
            <a href="#" className="btn btn-danger btn-sm">Delete</a>
        </td>
    </tr>
);

Disregard the React side of things and this is really straightforward.

And that's it for this stage. We have successfully retrieved (fetch'ed) all our blog post entries from our Symfony 3 API and rendered them out into our HTML table using React.

This proves our implementation is going to work - so long as we write some code that behaves as expected. In the next video we will tidy all this up, and start structuring our project a little better. We will also add some styles to make it look pretty - which is the most important part, I'm sure you will agree :)

Code For This Video

Get the code for this video.

Episodes