React Rating Star

Posted: Mon Sep 16 2024
thumbnail of article -  React Rating Star

Ratings are a common feature on websites, whether we're rating posts, games, movies, or products. That's why creating a rating component from scratch is a great idea. We can make use of useState, onClick, and mouse actions to handle interactions. Let’s get started!

post

First, in App.jsx, let's create a simple Star component that renders an SVG icon. We'll apply Tailwind CSS classes for sizing and default color. In the latest version of Tailwind, size-8 is equivalent to w-8 h-8, so we can use that for the star size.

const Star = () => {
	return (
		<svg
			className={`size-8 text-yellow-500`}
			aria-hidden='true'
			xmlns='http://www.w3.org/2000/svg'
			fill='currentColor'
			viewBox='0 0 22 20'>
			<path d='M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z' />
		</svg>
	)
}

Next, we’ll render 5 stars. While using a for loop might seem like a good idea, it’s not the best choice in this case. For loops are imperative, meaning we have to explicitly tell the program what to do step by step.

function App() {
	

  const stars = []; 


  for (let i = 1; i < 5; i++) {
    stars.push(
      <Star
        key={i}
      
      />
    );
  }

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex items-center'>
       {stars}
			</div>
		</div>
	)
}

In React, coding can sometimes feel imperative, where we explicitly write out each step of the solution. However, the main focus should be on achieving the desired functionality, such as implementing the rating feature.

In functional programming, we use functions like map to handle such tasks more elegantly. By using map, we can achieve the same result with significantly fewer lines of code compared to an imperative approach.

function App() {

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex items-center'>
				{[...Array(5)].map((_, index) => (
					<Star key={index} />
				))}
			</div>
		</div>
	)
}

This version is concise and clearly communicates the use of useState for managing star ratings.

function App() {
	const [rating, setRating] = useState(0)
	
console.log(rating);

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex items-center'>
				{[...Array(5)].map((_, index) => (
					<Star key={index} 
          handleRating={() => setRating(index + 1)} 
          />
				))}
			</div>
		</div>
	)
}

First, we use useState to keep track of which star has been clicked. To determine how many stars should be filled, we use the following approach: filled={index < rating}. This condition ensures that stars are filled based on the value stored in rating, which represents the number of stars that should be filled.

import { useState } from 'react'

const Star = ({ handleRating }) => {
	return (
		<svg
    onClick={handleRating}
			className={`size-8 ${filled ? 'text-yellow-500' : 'text-gray-300'} `}
			aria-hidden='true'
			xmlns='http://www.w3.org/2000/svg'
			fill='currentColor'
			viewBox='0 0 22 20'>
			<path d='M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z' />
		</svg>
	)
}

function App() {
	const [rating, setRating] = useState(1)
	
console.log(rating);

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex items-center'>
				{[...Array(5)].map((_, index) => (
					<Star key={index} 
          handleRating={() => setRating(index + 1)} 
          filled={index < rating}
          />
				))}
			</div>
		</div>
	)
}

export default App

Lastly, we’ll add a feature to fill the stars when hovering over them. For this, we’ll use React events like onMouseEnter and onMouseLeave.

First, we need to set up a new state called hovered, initially set to null. We also need to modify the filled property to account for this state: filled={index < (hovered !== null ? hovered : rating)}. This means that if hovered is not null, the number of filled stars is determined by the hovered state. Otherwise, it falls back to the rating state.

Additionally, we’ll create simple functions to handle mouse events and pass them as props to the Star component.

import { useState } from 'react'

// import Star from './components/Star'

const Star = ({ filled, handleRating,handleHoverFill,handleHoverClear }) => {
	return (
		<svg
			onClick={handleRating}
      onMouseEnter={handleHoverFill}
      onMouseLeave={handleHoverClear}
			className={`size-8 ${filled ? 'text-yellow-500' : 'text-gray-300'} `}
			aria-hidden='true'
			xmlns='http://www.w3.org/2000/svg'
			fill='currentColor'
			viewBox='0 0 22 20'>
			<path d='M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z' />
		</svg>
	)
}

function App() {
	const [rating, setRating] = useState(0)
	const [hovered, setHovered] = useState(null)

	console.log(hovered)

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex items-center'>
				{[...Array(5)].map((_, index) => (
					<Star
						key={index}
						handleRating={() => setRating(index + 1)}
						filled={index < (hovered !== null ? hovered : rating)}
						handleHoverFill={() => setHovered(index + 1)}
						handleHoverClear={() => setHovered(null)}
					/>
				))}
			</div>
		</div>
	)
}

export default App

Finally, we’ll move the code into a separate component. Once that’s done, we’ll have a fully functional star rating component!

import React from 'react'

const Star = ({ filled, handleRating, handleHoverFill, handleHoverClear, index }) => {
	return (
		<svg
			onClick={() => handleRating(index)}
			onMouseEnter={() => handleHoverFill(index)}
			onMouseLeave={handleHoverClear}
			className={`size-8 cursor-pointer ${filled ? 'text-yellow-500' : 'text-gray-300'}`}
			aria-label={`Rate ${index + 1} stars`}
			xmlns='http://www.w3.org/2000/svg'
			fill='currentColor'
			viewBox='0 0 22 20'>
			<path d='M20.924 7.625a1.523 1.523 0 0 0-1.238-1.044l-5.051-.734-2.259-4.577a1.534 1.534 0 0 0-2.752 0L7.365 5.847l-5.051.734A1.535 1.535 0 0 0 1.463 9.2l3.656 3.563-.863 5.031a1.532 1.532 0 0 0 2.226 1.616L11 17.033l4.518 2.375a1.534 1.534 0 0 0 2.226-1.617l-.863-5.03L20.537 9.2a1.523 1.523 0 0 0 .387-1.575Z' />
		</svg>
	)
}
export default Star
import { useState } from 'react'
import Star from './Star'

const StarRating = () => {
	const [rating, setRating] = useState()
	const [hovered, setHovered] = useState(null)

	return (
		<div className='flex justify-center items-center mt-32'>
			<div className='flex flex-col items-center gap-2'>
				<div className='flex gap-1'>
					{[...Array(5)].map((_, index) => (
						<Star
							key={index}
							handleRating={() => setRating(index + 1)}
							filled={index < (hovered !== null ? hovered : rating)}
							handleHoverFill={() => setHovered(index + 1)}
							handleHoverClear={() => setHovered(null)}
						/>
					))}
				</div>
				{rating && (
					<h2 className='text-lg'>
						Rating: <span className='font-semibold'>{rating}</span>
					</h2>
				)}
			</div>
		</div>
	)
}

export default StarRating
import StarRating from './components/StarRaing'

function App() {
	return (
		<>
			<StarRating />
		</>
	)
}

export default App

Code: https://github.com/marekgacek45/reactMaster/tree/main/starRating

See on YT: https://youtu.be/rE5vI_39uH8


Discover more