Skip to main content

Tutorial: StarRating Component

You will build a small Star Rating system during this tutorial. This tutorial does not assume any existing Pando or React knowledge. The techniques you'll learn in the tutorial are fundamental to building any React component and using Pando. Following this tutorial should help give you a deeper understanding of Pando.

This tutorial is designed for people who prefer to learn by doing and want to quickly try making something tangible.

What are you building?

In this tutorial, you'll build a StarRating component with Pando React.

You can see what it will look like when you're finished here:

function StarRating(props) {
  // Change the 0 to any number 1 - 5 to see stars filled
  const rating = useMemo(() => props.rating || 0, [props.rating])
  const ratingList = new Array(5).fill('')

  return (
    <Grid cols={12} gap={6}>
      <For each={ratingList}>
        {(_, idx) => (
          <GridItem key={`rating-${idx}`} colSpan={1}>
            <IconButton
              ariaLabel={`Rating ${idx + 1} out of 5`}
              icon={rating >= idx + 1 ? StarFilledIcon : StarIcon}
              onClick={props.onClick}
              usage="text"
            />
          </GridItem>
        )}
      </For>
    </Grid>
  )
}

There are a few important patterns that we are executing here, but the most important one we want to point out from the start is that we are creating a presentational component.

By allowing this component to have the single responsibility of only displaying the status, we help reduce the risks of bugs and allow this component to be more reusable throughout the life span of the code base.

When building components, remember that they are just functions, so you should approach them with the same clean code standards - having a single responsibility.

Setup your environment

We recommend creating a sandbox via CodeSandbox so you can dive as deep as you need with access to a full project environment (HTML, CSS, etc.).

If you are using CodeSandbox, create a new Sanbox with React or React/Typescript if you prefer a typed environment.

All of our libraries are built with Typescript, so we ship type helpers if you ever need them.

You can also follow this tutorial using your local development environment. To do so, create a new project using one of your favorite scaffolding tools: ViteJS and Create React App are great starting points.

Deconstructing the design

Normally, when you start building a component, you start with the design file. In Thinking in React, we learn how to divide each part of a design into single responsibility sections that will in turn become components.

Our StarRating design looks like there are 3 different parts:

  • Wrapper, which is a list of stars using a grid layout.

  • Each list item should be actionable via a click, so they should be buttons.

  • Each button will display either a filled or empty icon.

This means that the parent component should be responsible for fetching the data and the StarRating only responsible for displaying that data (presentational).

Add the Pando CSS Setup

In order to use Pando, we first need to install it! Open the index.html in your project and add the following to the bottom of the head section:

<link
  rel="preload"
  href="https://fonts.pluralsight.com/ps-tt-commons/v1/ps-tt-commons-variable-roman.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@pluralsight/design-tokens/fonts.css"
/>
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@pluralsight/design-tokens/npm/normalize/normalize.css"
/>

This adds the fonts, themes, and CSS resets Pando relies on.

Next, make sure you don't have any initial CSS being used. If you have content in a CSS file being imported into your App file, go ahead and remove the import so we can start fresh.

At this point, you should see the background color and typography change to use the Pluralsight Commons font and styles.

Installing Pando

Now that we have the CSS reset in place, we are ready to install Pando into our project.

npm install @pluralsight/{react,headless-styles,icons}

Once the packages are installed, we are ready to build!

Building the Grid

In your project, let's create a new file called StarRating. Inside that file, we will create the initial component and include the grid layout using the Pando Grid API.

import { Grid, GridItem } from '@pluralsight/react'

export function StarRating() {
  return (
    <Grid cols={12} gap={6}>
      <GridItem>Star</GridItem>
    </Grid>
  )
}

On line 1 we import the Pando Grid Components and on line 3 create our StarRating component.

Adding the IconButton

In Pando, there are different types of Button APIs for Accessibility reasons. Not only does Headless-styles add styling, it also adds all the Accessibility properties you need to make your app 100% accessbile! 🎉

Because we need a button element that only displays an Icon, we should use the IconButton component.

Add the following to your code:

import {
  Grid,
  Grid,
+ IconButton
} from '@pluralsight/react'
+ import { PlaceholderIcon } from '@pluralsight/icons'

export function StarRating() {
  return (
    <Grid cols={12} gap={6}>
      <GridItem colSpan={1}>
+       <IconButton ariaLabel="un-selected star" icon={PlaceholderIcon} usage="text" />
      </GridItem>
    </Grid>
  )
}

With the IconButton, we are using the ariaLabel and usage options. ariaLabel sets the aria-label attribute on the button element and usage tells the API to style the component like a "text" button.

Your result should now look like this:

function StarRating() {
  return (
    <Grid cols={12} gap={6}>
      <GridItem colSpan={1}>
        <IconButton
          ariaLabel="un-selected star"
          icon={PlaceholderIcon}
          usage="text"
        />
      </GridItem>
    </Grid>
  )
}

Adding the Icons

Now that we have the foundation created, let's add the final missing UI piece: Icons. Add the new Icons to your file:

import {
  Grid,
  GridItem,
  IconButton
} from '@pluralsight/headless-styles'
+ import { StarIcon, StarFilledIcon } from '@pluralsight/icons'

export function StarRating() {
  return (
    <Grid cols={12} gap={6}>
      <GridItem colSpan={1}>
+       <IconButton ariaLabel="un-selected star" icon={StarIcon} usage="text" />
      </GridItem>
    </Grid>
  )
}

Your result should now look like this:

function StarRating() {
  return (
    <Grid cols={12} gap={6}>
      <GridItem colSpan={1}>
        <IconButton ariaLabel="un-selected star" icon={StarIcon} usage="text" />
      </GridItem>
    </Grid>
  )
}

At this point, we have the UI ready for the logic, so let's add that in now!

Want to create a more performant pattern? Go ahead and create a new component in the StarRating file called StarButton can move the button contents into that! Now you have more single responsibility components!

When building components, it's easy to add more and more logic to a single component (like "page" components in an app). Because React was never meant to be a application level framework, this will cause major performance issues and bugs in your app. Instead, create smaller components that are more focused on a single responsibility. This will help you create more performant apps and make it easier to debug when things go wrong. Using Next or Remix will not solve this problem, so it's important to understand this concept when building React apps.

Creating a rating Array

Now that we have the presentational UI setup, let's start adding the logic to our component. To do this, we are going to create a dynamic Array and fill its contents with an empty String so React can successfully loop through it.

Update your code with the following:

import {
+ For,
  Grid,
  GridItem,
  IconButton
} from '@pluralsight/react'
import { StarIcon, StarFilledIcon } from '@pluralsight/icons'

export function StarRating() {
+ const ratingList = new Array(5).fill('')

  return (
    <Grid cols={12} gap={6}>
+     <For each={ratingList}>
+       {(_, idx) => (
         <GridItem key={`rating-${idx}`} colSpan={1}>
           <IconButton
             ariaLabel="un-selected star"
             icon={StarIcon}
             usage="text"
           />
         </GridItem>
+       )}
+     </For>
    </Grid>
  )
}

Here, we created a new constant called ratingList which creates a new Array of 5 items which are filled with an empty String. The fill method is important and is required by React in order to be able to map through a dynamic list.

Then, we added the For component from Pando and passed in the ratingList constant. The For component will memoize the data and loop through the list and return the contents of the children prop for each item in the list.

At this point, your result should look like this:

function StarRating() {
  const ratingList = new Array(5).fill('')

  return (
    <Grid cols={12} gap={6}>
      <For each={ratingList}>
        {(_, idx) => (
          <GridItem colSpan={1} key={`rating-${idx}`}>
            <IconButton
              ariaLabel="un-selected star"
              icon={StarIcon}
              usage="text"
            />
          </GridItem>
        )}
      </For>
    </Grid>
  )
}

Add the filled props

We are on the final step now of adding our props to each component! This is the easy part. We know that at the end of the day, the rating is just a number and in order to update the number and display the new rating, we need to click the button.

Add the following to your code:

import { memo } from 'react'
import {
  For,
  Grid,
  GridItem,
  IconButton
} from '@pluralsight/react'
import { StarIcon, StarFilledIcon } from '@pluralsight/icons'

function StarRatingEl(props) {
+ const rating = props.rating ?? 0
 const ratingList = new Array(5).fill('')

  return (
    <Grid cols={12} gap={6}>
      <For each={ratingList}>
        {(_, idx) => (
          <GridItem key={`rating-${idx}`} colSpan={1}>
            <IconButton
+             ariaLabel={`Rating ${idx + 1} out of 5`}
+             icon={rating >= idx + 1 ? StarFilledIcon : StarIcon}
+             onClick={props.onClick}
              usage="text"
            />
          </GridItem>
        )}
      </For>
    </Grid>
  )
}

+ export const StarRating = memo(StarRatingEl)

You only need to use memo on custom components that accept non-primitive properties. We are using it on StarRating because it will accept a Function for the click event which is not a primitive value.

Here we have added the props parameter to both StarRating and StarButton and included our new filled prop with the logic that will determine its state.

The result should look similar to below:

function StarRating(props) {
  // Change the 0 to any number 1 - 5 to see stars filled
  const rating = props.rating || 0
  const ratingList = new Array(5).fill('')

  return (
    <Grid cols={12} gap={6}>
      <For each={ratingList}>
        {(_, idx) => (
          <GridItem key={`rating-${idx}`} colSpan={1}>
            <IconButton
              ariaLabel={`Rating ${idx + 1} out of 5`}
              icon={rating >= idx + 1 ? StarFilledIcon : StarIcon}
              usage="text"
            />
          </GridItem>
        )}
      </For>
    </Grid>
  )
}

Where to go from here

This was a very brief introduction to building a component using Pando. You can start a Pando project right now or dive deeper on all the syntax used in this tutorial.