React - Sorting

This video is available to view for members only.

Click here to Join!

Already a member?

Login


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 ascending or descending 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.

Code For This Episode

Get the code for this episode.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Intro, and Project Demo 02:27
2 Pagination with Twig and KnpPaginatorBundle 07:49
3 Sorting with Twig and KnpPaginatorBundle 02:15
4 Simple Filtering in Twig 05:00
5 API - Pagination, (Basic) Filtering, and Sorting 10:25
6 Angular - Pagination 10:14
7 Angular - Sorting 06:11
8 Angular - Limiting 05:43
9 Angular - Filtering 03:47
10 React - Pagination (Part 1) 10:45
11 React - Pagination (Part 2) 04:14
12 React - Sorting 11:11
13 React - Limiting 03:43
14 React - Filtering 03:41