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 ;)