Star Widget



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
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.
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.
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:
- App.js: The parent component that manages the state of the star ratings
- StarRating.js: The reusable rating component that handles user interactions
- 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:
- Rating State: Managed in the parent App component using useState. This is the "source of truth" for the actual selected rating.
- 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)}
>
- Click Events: When a star is clicked, we call the
onChange
function (passed as a prop) with the new rating value (index + 1
). - Mouse Enter: When the mouse hovers over a star, we update the
hoveredIndex
state to that star's index. - Mouse Leave: When the mouse leaves a star, we reset the
hoveredIndex
tonull
.
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:
- If we're currently hovering over any star (
hoveredIndex !== null
):- Fill all stars up to and including the hovered star
- 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:
- Users can navigate between stars using the Tab key
- Users can select a star by pressing Enter when focused
- 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:
- Hovering over a star updates the
hoveredIndex
state, causing all stars up to that index to appear filled. - Moving the mouse away resets
hoveredIndex
to null, returning to showing the actual rating. - Clicking a star calls the
onChange
function with the new rating, which updates the parent component's state. - 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
andmouseLeave
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
inStarRating
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 theApp
level, to prevent unnecessary re-renders of allStarRating
components, thereby optimizing performance and maintaining component independence?
Component Architecture
Did I create separate components for
Star
andStarRating
?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 ofdivs
, 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