Debugging Detective: The Switcheroo Sort

Debugging Detective: The Switcheroo Sort

Here in the Debugging Detective series, I will occasionally share interesting and illuminating debugging experiences from my programming journey. I'll start by sharing my code and describing the unexpected behavior that resulted. This will likely be all that advanced code sleuths need to spot my initial error. For everyone else, however, I'll follow up with a deeper dive into the topics involved before revealing how I identified and eventually corrected the mistake. Enjoy!

There are lots of ways a bug can manifest itself. Most often, it's in the form of a big, scary error that brings the whole app to a standstill. The flip side of such a dramatic failure is that the fix is usually obvious, often spelled out right in the error message. Simply close a parentheses or fix a misspelled function call and you're back up and running. To be honest, I don't even think of these as bugs anymore - they're more like coding typos. Instead, the errors that interest me the most are the ones where at first glance everything seems fine, only for unexpected results to emerge as you use the app's features. These occasions highlight one of my favorite things about debugging: small mistakes can lead to interesting effects.

I made one such mistake while working on a React app where the user can sort "stocks" that are generated from a json-server backend. As part of the project, I was asked to fetch the stocks as JSON data, render them to the page using React's useState hook, and then incorporate sort and filter features. Fetching and rendering the stocks was straightforward, but once I tried to implement the sort feature, things got weird. Here's how things looked after my initial attempt:

Note that the initial click on either "Alphabetically" or "By Price" does nothing, while changing from one to the other with a subsequent click causes the other sort method to be applied. Along with unexpected results, I also like it when a bug has a sense of humor, so you can be sure that this one had my attention. Let's look at the code. In the form element, I passed a function called handleSortChange as an onChange prop. This means that whenever the user changes which radio button is selected, handleSortChange should be called with the event as its lone argument:

<label>
        <input
          type="radio"
          value="Alphabetically"
          name="sort"
          checked={null}
          onChange={handleSortChange}
        />
        Alphabetically
      </label>
      <label>
        <input
          type="radio"
          value="Price"
          name="sort"
          checked={null}
          onChange={handleSortChange}
        />
        By Price
      </label>

Below is my function body for handleSortChange. I should point out that sortBy and stocks are both state variables, which means that they are updated with the setSortBy and setStocks functions. The main idea is that we update sortBy, then use that value to determine how to sort the stocks array. Elsewhere in the app, React renders the newly sorted stocks array to show the changes.

function handleSortChange(e) {

    setSortBy(e.target.value)

    setStocks(stocks.sort((a, b) => {

      if (sortBy === "Alphabetically") {
        if (a.name < b.name) { return -1}
        if (a.name > b.name) { return 1}
        return 0
      }
      if (sortBy === "Price") {
        if (a.price < b.price) { return -1}
        if (a.price > b.price) { return 1}
        return 0
      }
      return 0
    }))
  }

Now is the time for experienced debuggers to try and spot my mistake. If you'd like to do so, the code above has everything you need. Otherwise, keep reading and I'll walk you through my own debugging voyage.

The two if statements are 'compare functions' that allow me to use JavaScript's built-in sort function on an array of objects such as my stocks. Feel free to read up on the documentation here if you'd like to learn more about compare functions. I could have accidentally swapped the two compare functions, which would cause the wrong sort to be applied. But that wouldn't explain why no sorting was done upon the first click. I double-checked each function and concluded that they weren't to blame.

The next thing I decided to inspect was the process of setting state. I remembered that all setState functions in React were asynchronous, including setSortBy. This means that the process of setting the new state continues in the background while the next lines of code are executed. Hundreds or even thousands of lines of code could theoretically run before the asynchronous function is fully carried out. To test my suspicion that this had something to do with my sorting bug, I added a few console log functions to my code:

function handleSortChange(e) {

    console.log(`hello from line 12, currently sorting by ${sortBy}`)
    setSortBy(e.target.value)
    console.log(`hello from line 14, still sorting by ${sortBy}`)
    setStocks(stocks.sort((a, b) => {

      if (sortBy === "Alphabetically") {
        if (a.name < b.name) { return -1}
        if (a.name > b.name) { return 1}
        return 0
      }
      if (sortBy === "Price") {
        if (a.price < b.price) { return -1}
        if (a.price > b.price) { return 1}
        return 0
      }
      return 0
      }))
    console.log(`on line 32, we continue to sort by ${sortBy}`)
  }

This let me see what the value of sortBy was at a few different places: from line 12 before I called setSortBy, on line 14 immediately after calling setSortBy, and on line 32 after calling setStocks, which updates the stocks state variable with the newly sorted stocks. Here's what the console looked like after the first click on "Alphabetical":

In other words, the setSortBy function still hadn't finished updating the sortBy state variable by the time the last lines of the handleSortChange function were executed. I left my setStocks function waiting for a message from the future that hadn't arrived yet. Here's what happened in the console when I then clicked on the "By Price" radio input:

That explained the switcheroo I was seeing earlier. In the time it took between me clicking one radio input and another, the setSortBy function finished its work of updating the sortBy state variable, long after the rest of the function body was executed. And because of the way I used these asynchronous setState functions, each click began the process anew and sorted using an outdated definition for sortBy.

Now that I had spotted the source of the error, I needed to figure out a way to fix it. Examining my code I realized that I had forgotten to check whether sortBy needed to be a state variable at all. React provides the following guidelines for deciding to make something state:

I had run afoul of the third guideline. I define sortBy as e.target.value, which returns the value of the radio input that was clicked. From there, I only used it in the conditional statements to determine which compare function to apply. I could easily just keep using e.target.value, rather than involving another state variable. Making that change to the code I have the following as my handleSortChange function:

function handleSortChange(e) {

    console.log(`hello from line 12, currently sorting by ${e.target.value}`)

    console.log(`hello from line 14, now sorting by ${e.target.value}`)
    setStocks(stocks.sort((a, b) => {

      if (e.target.value === "Alphabetically") {
        if (a.name < b.name) { return -1}
        if (a.name > b.name) { return 1}
        return 0
      }
      if (e.target.value === "Price") {
        if (a.price < b.price) { return -1}
        if (a.price > b.price) { return 1}
        return 0
      }
      return 0
      }))
    console.log(`way down in line 32, sorting by ${e.target.value}`)
  }

This looked much better, with one little problem: now the sort function didn't work at all. No matter how many times I clicked the radio inputs the stocks wouldn't change order. What a twist! The console told me that I had fixed the problem of delayed sorting values. Refreshing the page and clicking on "Alphabetically" gives me the expected console logs:

So why wasn't the page updating? It turned out there was one more rule about the inner workings of React that I needed to take into account. If your state variable points to an array, simply changing one or more of the elements in that array doesn't cause React to render the elements of the DOM that work with that state variable. In other words, sorting stocks and passing it to setStocks wouldn't change things in the DOM. I would need to send in a whole new array. Methods like filter and map do this automatically, and I forgot that other methods, including sort, modify the array in-place rather than returning a new array. Fortunately, there's a quick fix. By using the slice method, with no arguments, I can return a new copy of the stocks array adfter sorting, which triggers a re-render from React. Adding this last piece, and removing the console logs gets me a functional function:

  function handleSortChange(e) {

    setStocks(stocks.sort((a, b) => {

      if (e.target.value === "Alphabetically") {
        if (a.name < b.name) { return -1}
        if (a.name > b.name) { return 1}
        return 0
      }
      if (e.target.value === "Price") {
        if (a.price < b.price) { return -1}
        if (a.price > b.price) { return 1}
        return 0
      }
      return 0
      }).slice())
  }

And I get a sort functionality that works correctly right from the first click: