React - Sorting
In this video we are going to add a Sorting facility to our React CRUD application. This will allow the end user of our application to sort the id
and title
fields in asc
ending or desc
ending order.
To sort each column, all the user has to do is click the table column heading. This behaves identically to the Twig Sorting implementation, with the added benefit of not needing a full page refresh each time we sort our data.
Implementation Overview
Much like with how we handled the onSelect
prop
in our Pagination component, we will need to pass in the function from the list
container to our Table
component.
It's unlikely that we want to define multiple ways to sort our data, but by passing in a function from the outside, rather than defining / hard-coding a specific function directly inside the Table
, we give ourselves more options in terms of component composition.
Another way of putting this is that our Table
component knows it can do something called Sorting, but it is a table, it is not a sorter. It knows how to display a table, but it shouldn't really have knowledge of how it sorts the data it contains. You may disagree, that's fine.
By passing in a function - any function - we can tell our Table
to call the passed in function whenever we want to sort, without the Table
worrying about the sorting process at all.
With this in mind, we will create our sorting function inside the list
container, and then pass this as-yet-unrun function through to the Table
component via props
. I completely understand if this seems a little alien at first - it did to me too, but it does make your software waaay cooler in my opinion, and I don't just means for geeky reasons either. I mean, it makes your life easier doing things this way.
The only downsides to this approach - in my opinion - are that the initial learning curve is a little steeper, and figuring out why your function has lost its context (think: this
) is definitely initially confusing.
Future Proofing Our Setup
We have already added in Pagination.
As this is a course on Pagination, Filtering, Sorting (and also limiting), we might as well do some proactive work at this stage and start passing through all the possible parameters to our API URL builder, so we don't work in an inefficient way.
We already saw how to do this for Pagination:
// /src/actions/blogPostActions.js
import fetch from 'isomorphic-fetch';
export function fetchBlogPosts(page) {
let p = new URLSearchParams();
p.append('page', page || 1);
return fetch('http://api.symfony-3.dev/app_dev.php/posts?' + p, {
method: 'GET',
mode: 'CORS'
})
.then(res => res.json())
.catch(err => err);
}
Effectively giving us a URL of :
http://api.symfony-3.dev/app_dev.php/posts?page=1
Or some other page number if one was passed in. If any of this is unclear to you, please watch the previous video.
Adding in the extra parameters is fairly straightforward:
// /src/actions/blogPostActions.js
import fetch from 'isomorphic-fetch';
export function fetchBlogPosts(page, limit, filter, sort, direction) {
let p = new URLSearchParams();
p.append('page', page || 1);
p.append('limit', limit || 10);
p.append('filter', filter || '');
p.append('sort', sort || '');
p.append('direction', direction || '');
return fetch('http://api.symfony-3.dev/app_dev.php/posts?' + p, {
method: 'GET',
mode: 'CORS'
})
.then(res => res.json())
.catch(err => err);
}
Again, if you are unsure on any of this, each step has been explained in this series so far, but for the quickest catch up, this video on API Pagination, Filtering, and Sorting should bring you up to speed.
id
or bp.id
One point to note at this stage is that we will need to use the field names in a manner that is compatible with our Doctrine Query implementation.
This is a bit... meh, but it does make sense. The thing I don't like about it is it exposes parts of our database implementation that really are not the concern of the end user. This makes little difference to us in this instance, as we are the sole developer on the project - both front and back end (hey, you can now add "FullStack" to your CV :)) - but it's not so great in the real world. If anything, it's just confusing.
We could add in a bunch of mitigation for this, but honestly it would make the issue more confusing without - in this instance - offering any tangible benefit.
The main thing to note is that our URL to query our API will look something similar to this when sorting:
http://api.symfony-3.dev/app_dev.php/posts?page=1&sort=bp.title&direction=asc
Note the bp.title
part, where title
would be nicer. That's all I'm talking about here. From the front end, as a user, you would never even notice it. It's a developer thing. But we devs like stuff right, and this feels a bit messy.
Hooking Up The UI
At this stage we need to start 'capturing' clicks on our column headings and doing something interesting... that would be showing the sorted data, I imagine.
The first stage is to wrap our column headings inside span
tags. If we added the onClick
functionality to the column heading (th
) tags themselves, we would hit on a bug in an upcoming video when trying to add input
elements to our column headings - for filtering.
// /src/components/Table.js
// * snip
render() {
return (
<div>
<table className="table table-hover table-responsive">
<thead>
<tr>
<th>
<span onClick={() => this.sortingHandler('bp.id')}>id</span>
</th>
<th>
<span onClick={() => this.sortingHandler('bp.title')}>Title</span>
</th>
<th>Options</th>
</tr>
</thead>
<tbody>
* snip *
</div>
);
};
You can see here that we have added the two onClick
event handlers.
Each handler is a function that, when called, will invoke the this.sortingHandler
method, which we have not yet defined.
Notice here that this is where we pass in bp.id
/ bp.title
, rather than just id
/ title
as discussed above.
This is where I find the most confusing part of this whole set up to be.
The event handler on our Table
component is going to call a function which itself is going to call the function we pass in via props
.
Read that again if unsure, as I appreciate it's a bit of a mindbender.
Once it feels right in your head, we can continue on with the implementation - which will - hopefully - make it all clearer anyway.
Let's start by defining the sorting function inside our list
container. This is the function that we will pass into the Table
component:
// /src/containers/list.js
onSort(sortBy) {
console.log('list container -- sort by', sortBy);
}
We are defining a very basic function here to simply test that data is being passed through as expected. We will also need to pass through a direction
argument here (asc
/ desc
) but one thing at a time :)
With this function defined inside our list
container, we can pass it to the Table
component via props:
// /src/containers/list.js
render() {
let totalPages = Math.ceil(this.state.totalItems / this.state.numItemsPerPage);
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)}
onSort={this.onSort.bind(this)}
></Table>
</div>
);
}
A potentially confusing part about these prop
names is where they come from?
A Detour Into Some Good Practices
The answer is - you just make them up. You can literally use anything. In my case onSort
best describes the purpose of this property. I want to pass through a function that is called / invokved when the table's data is requested to be sorted.
The other question I had after figuring this out is:
Ok, so I expect to pass in a function as this prop
- how can I enforce that it is always a function or this component will blow up? In other words - how can I make my life, or the life of my fellow developers, easier?
Thankfully, there is an answer to this question in the form of React's PropTypes. Adding these is really not difficult and the benefits are many. In future videos I will be adding these in, though I admit they are a little noisy when first learning the initial concepts of React. I digress.
I realise this sub-heading makes it now appear that you have actually been on a tour of some Bad Practices :) I don't profess to be a React expert by any means. I just share what I know.
Pass Back
There's some odd rules in Football. Proper football by the way - the beautiful game: soccer.
Thankfully, in React, the similarly named equivalent is also really interesting. It's mind expanding, if you come from solely PHP. At least, that's how I found it.
Because we have passed in a function, to another function, we can use logic to determine when that function should be run, and what with (its parameters).
It's lovely.
Where do the parameters for these functions come from? User input.
You are reacting to change on the UI.
It's a really nice compliment for your back end PHP skills, in my opinion. And it will make you a better PHP developer in the process.
In our sortingHandler
code on Table
, we can call the function passed in via props
, and use the selected UI parameters (current state
) from the input.
// /src/components/Table.js
sortingHandler(sortBy) {
this.props.onSort(sortBy);
}
And as now we have some real input, we can properly implement onSort
:
// /src/containers/list.js
onSort(sortBy) {
console.log('list container -- sort by', sortBy);
this.getBlogPosts(
this.state.currentPageNumber,
this.state.limit,
this.state.filterBy,
sortBy,
direction
);
}
We still need to add in the direction
case.
Here is the code, for a better explanation of how we get to this point, please watch the video from the 6:00 mark onwards.
// /src/components/Table.js
sortingHandler(sortBy) {
this.setState({
sortBy: sortBy,
reverse: !this.state.reverse
});
this.props.onSort(sortBy, this.state.reverse ? 'desc' : 'asc');
}
And to ensure we hit the API with our expected paramaters:
// /src/containers/list.js
onSort(sortBy, direction) {
this.setState({
sortBy,
direction
});
this.getBlogPosts(this.state.currentPageNumber, this.state.limit, this.state.filterBy, sortBy, direction);
}
And this should all be good.
Fixing A Bug
But we have introduced a slight problem.
By not updating all the method signatures of our this.getBlogPosts
calls, we have inadvertently broken the expected behaviour from the UI.
This isn't a catastrophic bug - the code still runs. But it isn't behaving as expected.
To catch stuff like this in the real world - use unit tests in the first instance. They will help you think about how you structure your code to be as defensive as possible.
Defenses, segueing so smoothly back to my football analogy are an essential part of making your life easier.
They keep out bugs (balls, in the now straining analogy).
Fewer bugs means an easier life.
The easier your life, the more fun you have. The cooler the stuff you can work on.
And it's not that hard. Honestly. Just invest a solid two weeks of 10 minutes a night, and you will get it. I didn't say you wouldn't have to work for it, but what's two weeks if it shaves years of stress off your life?
The simple fix here is to update each of the method calls to this.getBlogPosts
. I leave that as a task up to you, or you are always free to read the code.
At this stage you would have a test, or set of tests in place to ensure this never regresses.
That said, one dev's bug is another dev's feature.