React - Refactoring


In the previous video we created a working prototype to demonstrate that our React code could talk to our Symfony 3 REST API. That all worked, but it wasn't set up in a way that would scale as our project grows. Let's start fixing that.

To begin with, we are going to quickly fix up the application's layout and styling. We've already done the hard work here in our original Twig implementation, and as we are re-using the HTML layout, we can simply copy / paste the CSS over as needed:

/* /app.css */
/* app css stylesheet */

body {
    padding-top: 90px;
}
.starter-template {
    padding: 40px 15px;
    text-align: center;
}

.social-metric-count {
    font-size: 26px;
    text-align: center;
}

.list-group-item {
    min-height: 60px;
}

That should handle all the CSS for our app - remember, we are using Bootstrap, so we get almost all the CSS done for "free".

Next, we need to update the index.html file to wrap our React div inside some standard / plain-old Bootstrap-aware HTML:

<!-- /index.html -->

<!doctype html>
<html>
    <head>
      <title>React CRUD</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="/app.css">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    </head>
    <body>

    <nav class="navbar navbar-default navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="/">React CRUD</a>
        </div>
      </div>
    </nav>

    <div class="container">

      <div class="row">
        <div class="col-sm-12">
          <div id="root"></div>
        </div>
      </div>

    </div><!-- /.container -->

    <script src="/static/bundle.js"></script>
  </body>
</html>

This essentially brings our React implementation in-line with the Angular and Twig implementations - in terms of look and feel, at the very least.

Our React application is going to look for a div with the id="root" attribute, and then render our App onto that div:

// /src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

That takes care of the visuals. Now, let's focus on the code.

Restructuring Our React Code

Now, the React ecosystem / best-practice guidance changes frequently so this may not be bang-up-to-date in terms of how things are done. But for the scale of this app, this will be fine.

I'm going to go with a three folder layout:

  • src/actions
  • src/components
  • src/containers

Actions

Inside the src/actions directory I am going to create a single file that handles interactivity with the API. The actions themselves will be individually export'ed JavaScript functions, which I can import into any other file that needs them.

An example of this would be:

// /src/actions/blogPostActions.js

import fetch from 'isomorphic-fetch';

export function fetchBlogPost(id) {
    return fetch('http://api.symfony-3.dev/app_dev.php/posts/' + id, {
        method: 'GET',
        mode: 'CORS'
    }).then(res => res.json())
    .catch(err => err);
}

In essence, a standalone 'action' that can be used elsewhere in the project, on an as-needed basis.

This project is using ES6, so I am able to use shorthand function syntax. For a better demonstration of this, watch the video around the 2m50s mark. If you prefer to read about things like this, I cannot recommend 'Understanding ECMAScript 6' by Nicholas C. Zakas enough.

This file is very similar to the Api.js file that we had during the Angular project, but in my opinion, is slightly easier to work with.

Components

Our React app will be made up of three components:

  • src/components/NotFoundPage.js
  • src/components/Table.js
  • src/components/Form.js

The way I think of these is that components may be given data (via props), or they may need no data at all.

For example, our 404 errors will be directed to the NotFoundPage.js. This 'page' will not need any data to render out its content.

However, the Table component will need some data, but it won't know how to get the data itself. It must be given the data to work.

The next logical question then would likely be: Then what gets the data?

Containers

In my React apps I use the concept of 'containers' to initiate the processes that actually 'get' the data. I put 'get' in inverted commas because we aren't just going to GET, but also POST, PUT, and DELETE also.

A container isn't directly responsible for talking to our API - instead, it pulls in the functions (as needed) from files in the src/actions directory.

Our container files will be aware of their current 'state', and they will pass this current state down to components.

We will need three container files:

  • src/containers/blogPosts/create.js
  • src/containers/blogPosts/list.js
  • src/containers/blogPosts/update.js

Later, we will add the code that handles deleting a blog post entry into our list.js file.

First Refactoring

From our prototype we have the raw code that GET's our blog post entries from the API, and also the majority of the rendering function to spit out these blog posts into our table.

Let's extract the fetch'ing code from the App.js file to the src/actions/blogPostActions.js file:

// /src/actions/blogPostActions.js

import fetch from 'isomorphic-fetch';

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

Extracting this from the App.js file will have stopped our table's body content from rendering. But because we set some initial state:

// /src/App.js

import React, { Component } from 'react';

export default class App extends Component {

    constructor(props) {
        super(props);

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

Then our page itself will still render out at this stage.

We don't actually want this state to be in our App file though. And we don't want to be directly rendering out the table either. So, we aren't done just yet.

Creating List.js

We know that we need to GET the blog posts in order to display them. We've created the fetchBlogPosts action to do just this. This code shouldn't live in the root file of our application.

As we will be talking to our API and then storing the response into the current state, in this instance I am going to create a new file in the containers directory.

I am going to cut / paste the table from the App's render method directly into the render method of our List.

I am also going to cut / paste the constructor from App to List:

// /src/components/Table.js

import React, { Component } from 'react';
import {fetchBlogPosts} from '../../actions/blogPostActions';

export default class List extends Component {

    constructor(props) {
        super(props);

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

    componentDidMount() {
        fetchBlogPosts()
            .then((data) => {
                this.setState(state => {
                    state.blogPosts = data;
                    return state;
                })
            })
            .catch((err) => {
                console.error('err', err);
            });
    };

    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.props.blogPosts && this.props.blogPosts.map((post, i) => {
                        return (
                            <tr key={i}>
                                <td>{post.id}</td>
                                <td>{post.title}</td>
                                <td>
                                    <a href="" className="btn btn-default btn-sm">Edit</Link>
                                    <a href="" className="btn btn-danger btn-sm">Delete</btn>
                                </td>
                            </tr>);
                    })}
                    </tbody>
                </table>
            </div>
        );
    }
}

At this stage we can go ahead and fix up the App file:

// /src/App.js

import React, { Component } from 'react';
import List from './containers/list';

export default class App extends Component {

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

            <List></List>
        </div>
    );
  }
}

We still aren't done, but this should all still be working.

Creating a Table Component

The table we are rendering out doesn't need to be directly tied to the state of blogPosts. Perhaps we may want to make our table more generic, allowing it to receive all kinds of different data.

In this instance, leaving the table inside the List's render method probably isn't the worst thing in the world. But it could be better, so let's make it better.

As mentioned earlier, if the component can either get by without any data, or can be created from passed in data, then in my applications I tend to put them into components. Our table can be refactored to use props instead of state, so that makes it an ideal candidate to become a component.

// /src/components/Table.js

import React, { Component } from 'react';

export default class Table extends Component {

    constructor(props) {
        super(props);
    };

    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.props.blogPosts && this.props.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</Link>
                                    <a href="" className="btn btn-danger btn-sm">Delete</btn>
                                </td>
                            </tr>);
                    })}
                    </tbody>
                </table>
            </div>
        );
    }
}

Note also here that in the video I use the <tr key={i}> whereas here I have swapped to <tr key={post.id}> which I believe is better practice. See the previous video write up for more on this.

The big change here is that instead of:

{this.state.blogPosts && this.state.blogPosts.map(post => {

We are instead relying on props:

{this.props.blogPosts && this.props.blogPosts.map(post => {

Therefore, to make this work we must tell the <Table> component what the blogPost props will be made up from. We need to do that inside our List container:

import React, { Component } from 'react';
import {fetchBlogPosts} from '../../actions/blogPostActions';
import Table from '../../components/Table';

export default class List extends Component {

    constructor(props) {
        super(props);

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

    componentDidMount() {
        fetchBlogPosts()
            .then((data) => {
                this.setState(state => {
                    state.blogPosts = data;
                    return state;
                })
            })
            .catch((err) => {
                console.error('err', err);
            });
    };

    render() {
        return (
            <div>
                <Table blogPosts={this.state.blogPosts}/>
            </div>
        );
    }
}

We've replaced the render'ing of the table directly with the component instance instead. To do this, we must make sure to import the Table - as seen at the top of this file.

Next, we know the Table expects some blogPosts, so we simply pass through the current state of our blogPosts as props, using a key of the same name.

This should work whether the fetch has happened or not, as just as before, we have the initial state of blogPosts as an empty array.

And with that we are done with our initial refactoring. Quite a lot of change, consider we have added no new functionality :)

Getting Familiar With ES6

This project uses JavaScript ES6 throughout. In my opinion, ES6 places JavaScript amongst the most accessibly powerful languages available to the modern developer (not just web developers, either).

The syntax takes a little getting used too, but there's some amazing stuff in there.

One of my favourite parts of ES6 is Destructuring, which takes a little getting used to but is so cool when you start using it. We may even soon see this in PHP 7.x.

I mentioned this earlier, but I strongly recommend 'Understanding ECMAScript 6' by Nicholas C. Zakas. This is a fantastic resource that is very readable, compared to many computer science / programming language tomes. It's also completely free to read online, so no excuses!

If you prefer a more practical / hands-on approach, I love these ES6 Katas. I try and do one a day. And whilst it's not immediately obvious at first glance, there is some ordering to these Katas - mouse over... and hey, you must have JavaScript enabled to do that ;)

Code For This Video

Get the code for this video.

Episodes