Star Widget

React Easy 30 minHackerRankAmazonAirbnb

Prompt

Could you create a reusable, accesible and user friendly Star Rating Widget in React that allows users to rate any product or service on a scale of 1 to 5 stars?

Requirements

  • Component should be customizable to accept different rating scales
  • Display the current rating
  • Enable users to hover over the stars to see the rating change
  • Select the rating when a user clicks on a star
  • Allow users to navigate through the stars using the 'Tab' key
  • Select the rating when a user presses the 'Enter' key while a star is focused

Demonstrate the StarRating component's reusability by using it twice in the same application with different ratings, such as serviceRating and productRating.

Note

  • You can use #aca8ff color to fill the star with when hovered over
  • StarIcon SVG is given in the starter code

Example

Playground

Hint 1

To solve this problem we want to generate stars of given length.

We can use Array.from({ length: 5 }).map((_, index) => <Star key={index} />) to do this.

Hint 2

Your logic needs to account for two distinct user interactions

Hovering Over Stars - What should happen when a user hovers over any of the stars? Think about how the appearance of the stars should change during hover. This interaction is temporary and should give a visual indication of what the rating would be if the user clicks.

No Hover - When the user is not hovering over the stars, how should the stars display the current rating? This state reflects the actual selected rating and should be different from the hover state.

Hint 3

The index of each star is crucial in this logic. How could you compare this index to the hover state and the current rating to decide if a star should be filled?

Solution

Explanation

Let's break down how our Star Widget solution works, exploring how React components, state management, and event handling come together to create this interactive rating system.

Component Structure

I have organized the code into three main components:

  1. App.js: The parent component that manages the state of the star ratings
  2. StarRating.js: The reusable rating component that handles user interactions
  3. Star.js: A simple presentational component for rendering individual stars

StarRating component is the main reusable component that handles the user interactions. It will accept maxStars, rating and onChange as props.

Now, it is responsibility of parent component which is App.js in this case to pass the correct values to the StarRating component.

It is similar to how you install a package and use it in your project. You don't know how it is implemented, but you know how to use it. However, in this case, we know how StarRating component is implemented.

State Management

The key to our Star Widget is how we manage state across components:

// In App.js
const [serviceRating, setServiceRating] = React.useState(0);
const [productRating, setProductRating] = React.useState(0);

// In StarRating.js
const [hoveredIndex, setHoveredIndex] = React.useState(null);

We're using two different types of state:

  1. Rating State: Managed in the parent App component using useState. This is the "source of truth" for the actual selected rating.
  2. Hover State: Managed locally in each StarRating component. This temporary state only affects the visual display during mouse hover.

This pattern of "lifting state up" to the parent component for important data while keeping UI-specific state local is a fundamental React pattern. It allows our StarRating component to be truly reusable - we can have multiple instances with different ratings.

Generating Stars Dynamically

To create a flexible number of stars based on the maxStars prop, we use this elegant pattern:

Array.from({ length: maxStars }).map((_, index) => (
<button
key={index}
onClick={() => onChange(index + 1)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
>
<Star
isFilled={
hoveredIndex !== null
? index <= hoveredIndex
: index < rating
}
/>
</button>
))

Array.from({ length: maxStars }) creates an array with the specified length, which we can then map over to create our star buttons. This approach is more flexible than hardcoding a specific number of stars.

Event Handling

Our solution handles three main types of events:

<button
onClick={() => onChange(index + 1)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
>
  1. Click Events: When a star is clicked, we call the onChange function (passed as a prop) with the new rating value (index + 1).
  2. Mouse Enter: When the mouse hovers over a star, we update the hoveredIndex state to that star's index.
  3. Mouse Leave: When the mouse leaves a star, we reset the hoveredIndex to null.

This creates the interactive experience where stars light up as you hover over them, and the selection is only confirmed when you click.

Conditional Rendering Logic

The most elegant part of our solution is how we determine whether each star should be filled:

isFilled={
hoveredIndex !== null
? index <= hoveredIndex
: index < rating
}

This ternary expression checks:

  1. If we're currently hovering over any star (hoveredIndex !== null):
    • Fill all stars up to and including the hovered star
  2. If we're not hovering (hoveredIndex === null):
    • Fill all stars up to but not including the current rating

This creates the effect where hovering shows a "preview" of your selection, and the actual selection is displayed when not hovering.

Remember, the rating is only set when a user clicks on a star.

Accessibility

We've made our Star Widget accessible by using proper semantic HTML:

<button
key={index}
onClick={() => onChange(index + 1)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
>
<Star isFilled={...} />
</button>

By wrapping each star in a <button> element:

  1. Users can navigate between stars using the Tab key
  2. Users can select a star by pressing Enter when focused
  3. Screen readers will announce the elements as interactive buttons

I have come across a common mistake that candidates make. They use div or span elements to create the stars, then they use onKeyDown event on the div or span element to handle the key presses. By using button element, we can handle the key presses by default. It is better to use button element for this kind of interaction and remove any default styles which are not required for the button element. In our case it is background, border and padding.

CSS Styling

Our styling is clean and straightforward:

.star-icon {
width: var(--icon-size);
height: var(--icon-size);
cursor: pointer;
}

.star-icon-filled {
fill: var(--icon-color);
}

We use CSS variables (--icon-size and --icon-color) to make the styling configurable, and we apply the star-icon-filled class conditionally based on the isFilled prop to change the star's appearance.

Performance Optimization

Notice the use of React.memo in our StarRating component:

export default React.memo(StarRating);

This prevents unnecessary re-renders of the StarRating component when its props haven't changed. For example, if you have multiple StarRating components and only one changes, the others won't re-render.

Putting It All Together

When a user interacts with our Star Widget:

  1. Hovering over a star updates the hoveredIndex state, causing all stars up to that index to appear filled.
  2. Moving the mouse away resets hoveredIndex to null, returning to showing the actual rating.
  3. Clicking a star calls the onChange function with the new rating, which updates the parent component's state.
  4. The parent component passes the updated rating back down as a prop, completing the data flow cycle.

This creates a smooth, intuitive user experience with clean, maintainable code that follows React best practices.

Common Pitfalls

Creating an Array of given length: A common issue that developers run into when trying to create an array of a given length in JavaScript is that they might attempt to use the Array constructor directly and then try to iterate over it using methods like map. However, this won’t work as expected because the constructor creates an array with undefined in each position, but the array is still "empty" in a sense that the map function doesn't recognize the undefined values as valid elements to iterate over.

Let's look at the common mistake:

const arr = new Array(5);
arr.map((_, index) => index); // Does not work as expected

In the above code, an array of length 5 is created, but map does not iterate over the elements because they are not "defined".

The correct way is to use Array.from method which creates a new array instance from a given array or iterable object. Here’s how you can use Array.from to create an array of a given length and then map over it:

const arr = Array.from({ length: 5 }, (_, index) => index); // [0, 1, 2, 3, 4]

Wrong State Management: Here, the StarRating component is designed to be reusable, with its state (rating) managed by the parent component (App). However, a common mistake candidates make is maintaining the rating state inside the StarRating component itself, which significantly reduces the component's reusability.

If multiple StarRating components are needed with different ratings (as in the App component example), the internal state approach would require each StarRating instance to manage its own state separately, which is not ideal.

Enhancing Accessibility: If you have constructed the stars using the <button/> HTML element, they should already be focusable, supporting keyboard interaction. This is one of the key benefits of using the <button/> element, as it will naturally handle 'Enter' key presses without requiring any additional key event handler. However, if you've created the stars using <div/> or <span/> elements, you will need to apply the tabIndex attribute to make them focusable.

I would highly recommend you to read the article called How (Not) to Build a Button

My Attempt

Interviewer Criteria

HTML/CSS

  • Did my layout accurately match the provided design?

  • Did I use semantic HTML elements (button) for stars to ensure accessibility?

  • Were my CSS class names descriptive and meaningful?

  • Did I implement smooth transitions for hover effects?

JavaScript

  • Do I have a solid grasp of ES6 features like let, const, fetch, arrow functions, destructuring, etc.?

  • Do I understand and correctly apply mouseEnter and mouseLeave events?

  • Is my logic for updating the star rating on hover and click clear and concise?

  • Is my approach to generating the number of stars as per the maxStars prop clean and efficient?

React

  • Did I make effective use of the useState hook for managing the state of the star ratings?

  • Have I correctly implemented the concept of lifting state from StarRating to App?

  • Did I use React.memo in StarRating to optimize performance and prevent unnecessary re-renders?

  • Have I ensured that the hover state is maintained locally within each StarRating component, rather than at the App level, to prevent unnecessary re-renders of all StarRating components, thereby optimizing performance and maintaining component independence?

Component Architecture

  • Did I create separate components for Star and StarRating?

  • Did I avoid prop drilling and maintain clean data flow?

  • Were my component and file names intuitive and clear?

  • Did I make the component reusable with configurable maxStars?

Accessibility

  • Have I ensured accessibility in the star rating widget by using button elements instead of divs, thus supporting key features like focus management with the tab key?

  • Did I provide visual feedback for focus states?

Time Checkpoints

  • 10:00 AM

    Interview starts

  • 10:03 AM

    Interviewer provides the prompt

  • 10:05 AM

    Candidate reads the prompt, asks clarifying questions, and begins coding

  • 10:07 AM

    StarWidget component created and HTML written

  • 10:12 AM

    Star component created and styled

  • 10:14 AM

    Stars generated using Array.from()

  • 10:16 AM

    Styling completed for Filled Star

  • 10:17 AM

    Support added in StarWidget for maximum stars using maxStars prop

  • 10:22 AM

    Hovered state handling completed using hoveredIndex state with useState

  • 10:24 AM

    Support added for passing rating and onChangeRating to StarWidget component

  • 10:28 AM

    Core logic added to select the star

  • 10:30 AM

    Interview ends

00:00