devYuraKim / react

udemy - jonas schmedtmann
0 stars 0 forks source link

Re07-usepopcorn > src > StarRating | Setting different styles based on onClick event #7

Closed devYuraKim closed 2 months ago

devYuraKim commented 2 months ago
import { useState } from "react";

const containerStyle = {
  display: "flex",
  alignItems: "center",
  gap: "16px",
};

const starContainerStyle = {
  display: "flex",
};

function StarRating({ maxRating = 5 }) {
  const [rating, setRating] = useState(0);
  const [isFilled, setIsFilled] = useState(false);

  function handleRating(i) {
    setRating(i + 1);
    if (i <= rating) setIsFilled(true);
  }

  return (
    <div style={containerStyle}>
      <div style={starContainerStyle}>
        {Array.from({ length: maxRating }, (_, i) => (
          <Star key={i} onClick={() => handleRating(i)} isFilled={isFilled} />
        ))}
      </div>
      <p>{rating}</p>
    </div>
  );
}

export default StarRating;

// 1. Array.from()으로 array object를 생성
// 2. {length:5}는 길이만 5의 값을 가진 객체를 생성
// 3. (_, i)가 될 수밖에 없는 것이 2에서 생성한 객체는 element가 없고 length만 있어서 index만 정의 가능하기 때문
// 결론. Array.from(source, (element, index) => processResult)

const starStyle = {
  width: "48px",
  height: "48px",
  display: "block",
  cursor: "pointer",
};

function Star({ onClick, isFilled }) {
  return (
    <span role="button" style={starStyle} onClick={onClick}>
      {isFilled ? (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 20 20"
          fill="yellow"
          stroke="black"
        >
          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
        </svg>
      ) : (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 20 20"
          fill="white"
          stroke="black"
        >
          <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
        </svg>
      )}
    </span>
  );
}
devYuraKim commented 2 months ago

TL;DR: isFilled is a DERIVED STATE

okay, so the solution was simpler than I had imagined. Rather than using state, just set a boolean flag: isFilled={rating > i}

Mind that the visual change (whether a star is filled or not) is directly derived from the rating state. There’s no need for an additional state variable (isFilled) because you can dynamically calculate if a star should be filled when rendering.

Also, since the UI (stars' filled state) now depends directly on the rating, React will re-render the component immediately after setRating is called, and the stars' appearance will update accordingly.

devYuraKim commented 2 months ago

TL;DR: asynchronous setState running with stale data

Because the state updates in React are asynchronous, my isFilled logic was running before the rating state was actually updated. This is what led to two main problems:

  1. First click issue: On the first click, rating hadn’t been updated yet when the condition if (i <= rating) ran. So isFilled didn’t update properly based on the new rating, leading to the stars not being filled.
  2. Second click issue: After the first click, React re-rendered the component with the updated rating, but since isFilled had already been set, the next time the logic ran, it affected all the stars, causing them all to change color.

But the logic itself if (i <= rating) setIsFilled(true) isn't fundamentally incorrect in concept. But I don’t need to store the result in a separate state (isFilled). Instead, I can derive this value directly from the current rating during rendering.