Using React's useState Hook for Sorting

How many times have we come across a "click to sort" style feature in our day-to-day interactions with web applications? Think: Sort Price Low to High, Alphabetizing, Most Recent Transactions etc. When building in React, we can use state to handle this. For simplification, let's hard code in a collection of transaction objects:

const transactions = [
    {
      "id": 1,
      "date": "2019-12-01",
      "category": "Income",
      "amount": 1000
    },
    {
      "id": 2,
      "date": "2019-12-02",
      "category": "Transportation",
      "amount": -10.55
    },
    {
      "id": 3,
      "date": "2019-12-04",
      "category": "Fashion",
      "amount": -24.99
    },
    {
      "id": 4,
      "date": "2019-12-06",
      "category": "Food",
      "amount": 8.75
    },
    {
      "id": 5,
      "date": "2019-12-06",
      "category": "Housing",
      "amount": -17.59
    }
]

These objects are rendered to the page as a <table> like so (don't forget to add a key prop when iterating over an array with the .map() method):

function App() {
  return (
    <table>
      <tbody>
        <tr>
          <th>
            <h3>Date</h3>
          </th>
          <th>
            <h3>Category</h3>
          </th>
          <th>
            <h3>Amount</h3>
          </th>
        </tr>
        {transactions.map(transaction => {
          return (
            <tr key={transaction.id}>
              <td>{transaction.date}</td>
              <td>{transaction.category}</td>
              <td>{transaction.amount}</td>
            </tr>
          )})}
      </tbody>
    </table>
  );
}

So our table should look like this:

Great! Now, we want to be able to click the "Category" header and have the items sorted alphabetically. To do that, we'll need to use a click event on the header cell. Let's write out a handler function for this event that takes an event object as an argument, and then add it as the callback function to a click event listener on the header cell :

function App() {
  function onHeaderClick(e) {
    let type = e.target.textContent.toLowerCase();
    const sorted = [...transactions].sort((a, b) => (a[type] > b[type]) ? 1 : ((b[type] > a[type]) ? -1 : 0))
  }

  return (
    <table>
      <tbody>
        <tr>
          <th>
            <h3>Date</h3>
          </th>
          <th>
            <h3 onClick={onHeaderClick}>Category</h3>
          </th>
          <th>
            <h3>Amount</h3>
          </th>
        </tr>
        {transactions.map(transaction => {
          return (
            <tr key={transaction.id}>
              <td>{transaction.date}</td>
              <td>{transaction.category}</td>
              <td>{transaction.amount}</td>
            </tr>
          )})}
      </tbody>
    </table>
  );
}

In the callback function, we've told Javascript to take the text content of the clicked header cell, set it to all lowercase so it matches the corresponding key in each of these objects (since it's capitalized in the header cell), then use the sort function on a copy of our transaction array to put the objects in the correct order by comparing the values at each "category" key. Because the .sort() method mutates the array, we use the spread operator to copy the array of objects over because we never want to directly mutate state.

Now that we've done that, when we click the header cell nothing happens—why? We have this newly sorted transaction list, but it's not connected to anything. To force the app to re-render the component with the sorted list, we'll need to use useState and update state value. First, let's import useState into our project and set the initial value to our hard coded transactions list (for the sake of this example, we aren't persisting any changes to a server).

import { useState } from "react";

function App() {
  const [ transactions, setTransactions ] = useState([
    {
      "id": 1,
      "date": "2019-12-01",
      "category": "Income",
      "amount": 1000
    },
    {
      "id": 2,
      "date": "2019-12-02",
      "category": "Transportation",
      "amount": -10.55
    },
    {
      "id": 3,
      "date": "2019-12-04",
      "category": "Fashion",
      "amount": -24.99
    },
    {
      "id": 4,
      "date": "2019-12-06",
      "category": "Food",
      "amount": 8.75
    },
    {
      "id": 5,
      "date": "2019-12-06",
      "category": "Housing",
      "amount": -17.59
    }
  ]);

  function onHeaderClick(e) {
    let type = e.target.textContent.toLowerCase();
    const sorted = [...transactions].sort((a, b) => (a[type] > b[type]) ? 1 : ((b[type] > a[type]) ? -1 : 0))
  }

  return (
    <table>
      <tbody>
        <tr>
          <th>
            <h3>Date</h3>
          </th>
          <th>
            <h3 onClick={onHeaderClick}>Category</h3>
          </th>
          <th>
            <h3>Amount</h3>
          </th>
        </tr>
        {transactions.map(transaction => {
          return (
            <tr key={transaction.id}>
              <td>{transaction.date}</td>
              <td>{transaction.category}</td>
              <td>{transaction.amount}</td>
            </tr>
          )})}
      </tbody>
    </table>
  );
}

export default App;

So we've set the initial value of our transactions to our hard coded original list. How do we update that variable with the the newly sorted list? By adding a setTransactions call to our click event, we can update the value of transactions (remember: we never want to directly mutate state):

function onHeaderClick(e) {
  let type = e.target.textContent.toLowerCase();
  const sorted = [...transactions].sort((a, b) => (a[type] > b[type]) ? 1 : ((b[type] > a[type]) ? -1 : 0))
  setTransactions(sorted);
}

When we updated state, the App component re-rendered with the sorted list as the new value of transactions, which caused the mapping method in the return function to iterate over the newly-ordered array. Our new sorted table looks like this:

Awesome! What's great about this process is that with some tweaks to the click event logic, you can attach this event handler to multiple headers, such as sorting the list by most expensive to least expensive, sorting by date, etc. Adding a new toggle state could allow you to switch between ascending and descending order. One of the most widely used features now simplified!

19