Junior v. Senior React Code: Using Flags with React Components

Junior v. Senior React Code: Using Flags with React Components

This post is one in a series in which we take React code that a less-experienced developer would write and improve it so that it resembles more what a professional would write. In the first part of the series, we converted a class component to a function component.

Today we're covering best practices for naming and using flags that you pass into components.

The junior code

(If you've already read the other post in the series, you can skip this section.)

Below is the component that we’re going to refactor. Here is a link to an interactive version.

import React, { Component } from "react";

export default class ItemList extends Component {
  state = { items: [], category: null };
  componentDidMount() {
    this.getData(true).then(({ urls, category }) => {
      this.setState({ items: urls, category });
    });
  }

  loadMore = () => {
    this.getData(false).then(({ urls }) => {
      this.setState({ items: this.state.items.concat(urls) });
    });
  };

  getData = (isInitialLoad) => {
    // This function loads iamges of cats, and if it is initial load, then it determines the
    // first category so that it keeps loading the same category
    const limit = isInitialLoad ? 20 : 10;
    return fetch(
      `https://api.thecatapi.com/v1/images/search?limit=${limit}${
        isInitialLoad ? "" : `&category_ids=${this.state.category}`
      }`
    )
      .then((res) => res.json())
      .then((responses) => {
        let urls = responses.map((data) => data.url);
        if (isInitialLoad) {
          const categories = responses.reduce((acc, resp) => {
            if (resp.categories) {
              acc = acc.concat(resp.categories);
            }
            return acc;
          }, []);
          const category = categories[0].id;
          return { urls, category };
        }
        return { urls };
      });
  };

  render() {
    return (
      <div>
        {this.state.items.map((item, ind) => (
          <img
            src={item}
            width={400}
            height={300}
            key={ind}
            alt={`pic-${ind}`}
          ></img>
        ))}
        <button onClick={this.loadMore}>Load more</button>
      </div>
    );
  }
}

Below is the refactored version of the same code.

Click here for a live version.

import React, { useState, useEffect, Fragment } from "react";

const baseURL = "https://api.thecatapi.com/v1/images/search";
const initialLoadNumber = 20;
const normalLoadNumber = 10;

export const ItemList = () => {
  const [urls, setUrls] = useState([]);
  const [category, setCategory] = useState(null);
  const [isInitialLoad, setIsInitialLoad] = useState(true);

  useEffect(() => {
    if (isInitialLoad) {
      setIsInitialLoad(false);
      loadInitialRandomCategoryImages().then(({ urls, category }) => {
        setUrls(urls);
        setCategory(category);
      });
    }
  }, [isInitialLoad]);

  const loadInitialRandomCategoryImages = () => {
    return fetch(`${baseURL}?limit=${initialLoadNumber}`)
      .then((res) => res.json())
      .then((responses) => {
        const urls = responses.map((data) => data.url);
        const imgWithCategory = responses.find((resp) => resp.categories);
        const category = imgWithCategory
          ? imgWithCategory.categories[0].id
          : null;
        return { urls, category };
      });
  };

  const loadMoreSameCategoryImages = () => {
    return fetch(
      `${baseURL}?limit=${normalLoadNumber}&category_ids=${category}`
    )
      .then((res) => res.json())
      .then((responses) => {
        const urls = responses.map((data) => data.url);
        return { urls };
      });
  };

  const loadMore = () => {
    loadMoreSameCategoryImages().then(({ urls: newUrls }) => {
      setUrls(urls.concat(newUrls));
    });
  };

  return (
    <Fragment>
      {urls.map((item, ind) => (
        <img
          src={item}
          width={400}
          height={300}
          key={ind}
          alt={`pic-${ind}`}
        ></img>
      ))}
      <button onClick={loadMore}>Load more</button>
    </Fragment>
  );
};

export default ItemList;

The main thing the component does is fetch cat pictures and displays them. At the bottom is a button that when clicked will load more photos. There is just one somewhat complicated feature. When the user clicks the “Load more” button, we only want to show photos in the same category as the first photo that was shown when the component initially rendered, for example "cats in hats" or "kittens". This is a strange design choice, but let’s just pretend that the designer on this project has some theory about how that feature will increase engagement so we have to implement it.

Passing a boolean into a function

Whenever you pass a boolean as an argument to a function, it is likely that the code can be better. The big reason is that it is hard to understand what effect that argument has on the function. This type of code is called a "code smell". It is not a bug, but it indicates that there is a deeper problem in the program.

If you want to learn more about code smells, we would highly recommend reading the book Clean Code. You might disagree with some of the things he argues, but the book is worth reading once in your life. In the book, he lists lots of examples of code smells, and this type is called selector argument. If you encounter a situation where you want to use a boolean as a flag, you probably should split the function into two different functions.

Before I show you how to split this code into two functions, I will explain how you can make the code better if you want to keep using the boolean as a flag.

  1. Create a variable

When passing a boolean into a function, one problem is that you don’t know what that boolean means. The cheapest fix is to create a variable.

cosnt isInitialLoad = true;
this.getData(isInitialLoad).then(({ urls, category }) => {
  this.setState({ items: urls, category });
});

2. Use argument destructuring

es6 lets you unpack the arguments passed into a function, and we can leverage that here. This approach is a slight improvement because it takes one fewer line of code.

this.getData({isInitialLoad: true}).then(({ urls, category }) => {
  this.setState({ items: urls, category });
});
...
getData = ({isInitialLoad}) => {
...

Creating separate functions

The above two solutions are okay when the function is super simple. However, a lot of people would argue that we should always have two different functions. They would reason that the getData function does two different things depending on whether it is the initial load or not, so it is better to split this function into two. In the refactored, senior version of the code we split getData into loadInitialRandomCategoryImages and loadMoreSameCategoryImages. Now it is totally clear what each function is doing!


You can find part one in this series here.

If you liked this post, we recommend you subscribe to the Antcode blog (see below) and follow us on Twitter at @o_nagen and @mkinoshita12.

If you want to get reviews on your code and learn from reviews on other people’s code, we recommend that you check out what we’re building at Antcode and join our code review Slack group.

Happy coding!

Show Comments