DEV Community

Eve Porcello
Eve Porcello

Posted on • Updated on

Refactoring the StarRating Component with React Hooks

In Spring 2017, we released our first O'Reilly book, Learning React. On the day that I heard they were sending the final version of the book to the printer, I made one simple request on Twitter:

I thought this was very reasonable. Don't change anything for 5 years. Well, two years later a lot has changed, and to be fair, I was warned:

Clearly, no one listened to my tweet but for good reason. As React evolves, it continues to get better. We can leverage the new benefits by incorporating new patterns into our existing applications. In the first edition of the book, we built an app called the Color Organizer. It's an app that allows people to add, edit, and delete from their list of favorite colors. A key feature of the app was ratings where people could select a star rating for each color.

Let's take a look at the Color Organizer as it was in the first edition, and then we'll upgrade it to use the latest syntax for the second edition of the book that will come out later this year. (And yes, that is a nonchalant announcement that we are working on a 2nd edition of Learning React, which will contain a fresh update of all of the content. It's going to be awesome.)

The UI for the Color Organizer looks like this:

color-organizer

There are several components at work here, but let's focus on the <StarRating /> component:

star rating

The rating is made up of 2 components: the <StarRating /> and the <Star />. We need a way to track whether the star is selected to determine the rating. We also need to allow users to click on a star to apply a specific rating. Finally, we want to assign a value for a total number of stars so that this component could be used in apps where there was a 4 star rating, 5 star rating, 10 star rating, etc.

In the book, we used a class component for the StarRating and a function component for the Star. The Star looked like this:

const Star = ({ selected = false, onClick = f => f }) => (
  <div className={selected ? "star selected" : "star"} onClick={onClick} />
);

The Star has two properties: selected and onClick. If selected is false, it will have a className of star meaning that it will be gray instead of red. When clicked, the onClick method is invoked.

Next, let's look at the StarRating. This component will render the number of Star components from the totalStars property. When clicked, each Star will trigger the change method which will set the state of starsSelected. As is typical of class components, we need to handle setup in the constructor: setting an initial state and then binding this for the change event.

class StarRating extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      starsSelected: 0
    };
    this.change = this.change.bind(this);
  }

  change(starsSelected) {
    this.setState({ starsSelected });
  }

  render() {
    const { totalStars } = this.props;
    const { starsSelected } = this.state;
    return (
      <div className="star-rating">
        {[...Array(totalStars)].map((n, i) => (
          <Star
            key={i}
            selected={i < starsSelected}
            onClick={() => this.change(i + 1)}
          />
        ))}
        <p>
          {starsSelected} of {totalStars} stars
        </p>
      </div>
    );
  }
}

render(
  <StarRating totalStars={5} />,
  document.getElementById("react-container")
);

You can also check this out on CodeSandbox:

This looks pretty good, but we can make it better with Hooks.

Refactoring with Hooks

When setting out to refactor these components, we thought about a few main steps:

  1. Figure Out How to Update the Star Function
  2. Refactor Class Component as a Function Component
  3. Create a Hook for Selecting a Star Rating

1. Figure Out How to Update the Star Function

This step was the easy one. Since the Star is already a function, we didn't have to change it at all. Thanks to Alex and Eve from 2017 on that one.

const Star = ({ selected = false, onClick = f => f }) => (
  <div className={selected ? "star selected" : "star"} onClick={onClick} />
);

2. Refactor Class Component as a Function Component

Next, let's refactor the class component and make it a function. We'll start with ReactDOM.render. We want to render the StarRating and pass it a property: totalStars:

render(<StarRating totalStars={5} />, document.getElementById("root"));

Next up, we'll refactor the StarRating component, passing it totalStars as a property:

const StarRating = ({ totalStars }) => {
  return (
    <div className="star-rating">
      {[...Array(totalStars)].map((n, i) => (
        <Star
          key={i}
          selected={i < starsSelected}
          onClick={() => this.change(i + 1)}
        />
      ))}
      <p>
        {starsSelected} of {totalStars} stars
      </p>
    </div>
  );
};

We need to update the onClick handler for the Star because the StarRating function does not have access to this. Instead, we'll handle this with a hook:

3. Create a Hook for Selecting a Star Rating

Hooks allow you to use state without writing a class. When the StarRating was a class, we had a state variable for starsSelected and a method called change to set the state of starsSelected to something else. We'll use this same state variable in the function component:

import React, { useState } from "react";

const StarRating = ({ totalStars }) => {
  const [starsSelected] = useState(0);
  return <div className="star-rating">...</div>;
};

The hook we're using is called useState, which allows us to add state to the function component. We'll use the same state variable called starsSelected and set its initial value to 0 by passing it into the useState function. In other words, when the component renders, the initial state of starsSelected will be 0.

Next, we'll need a way to change the value of starsSelected when a new rating is given. This used to be called change in the class component, but let's make this a bit more descriptive and call it selectStar:

const StarRating = ({ totalStars }) => {
  const [starsSelected, selectStar] = useState(0);
  return <div className="star-rating">...</div>;
};

Then we'll replace the change function in the onClick to use the selectStar method:

const StarRating = ({ totalStars }) => {
  const [starsSelected, selectStar] = useState(0);
  return (
    <div className="star-rating">
      {[...Array(totalStars)].map((n, i) => (
        <Star
          key={i}
          selected={i < starsSelected}
          onClick={() => selectStar(i + 1)}
        />
      ))}
      <p>
        {starsSelected} of {totalStars} stars
      </p>
    </div>
  );
};

Pretty slick, right? We didn't have to make too many changes at all. Just a quick refactor of the component to be a function and the creation of a hook. The resulting component has less code and is more readable.

Here's the whole thing on CodeSandbox.

When Hooks were announced at ReactConf, there was a mad rush to start using them as much as possible. That makes sense because they are very cool. You don't need to rush though. Start by refactoring a few small components, perhaps components that you are very familiar with. We've written this StarRating countless times in our book and in our workshops. It's exciting to see this old friend look better than ever with Hooks.

Top comments (6)

Collapse
 
michaeltharrington profile image
Michael Tharrington

Hey Eve!

This looks really helpful. Cool idea to share bits of the book as you rewrite it!

Anyway, I just wanted to point out that there's a couple of broken images in this article in case you want to try loading them back in, starting under:

"The UI for the Color Organizer looks like this:"

Again, awesome post!

Collapse
 
eveporcello profile image
Eve Porcello

Thank you so much! Just fixed those!

Collapse
 
michaeltharrington profile image
Michael Tharrington

No problem!

Collapse
 
scottiegggg profile image
scottieGGGG

The article near the ends says "Here's the whole thing on CodeSandbox".. but there's not link? (what am i missing?)

Collapse
 
eveporcello profile image
Eve Porcello

You should be able to see it now!

codesandbox.io/embed/v0n20v6143

Collapse
 
scottiegggg profile image
scottieGGGG

Perfect..thanks!