ByteGrad / Professional-React-and-Next.js-Course

This repo contains everything you need as a student of the Professional React & Next.js Course by ByteGrad.com
https://bytegrad.com/courses/professional-react-nextjs
111 stars 58 forks source link

Fancy Counter: Better way to handle event listeners and solving the button focus issue without polluting the click event handlers #3

Open Dev-Dipesh opened 4 months ago

Dev-Dipesh commented 4 months ago

Problem 1: Solving the button focus issue conflicting with the space bar event

Solution:

We can simply use e.preventDefault() in the if conditional block.

const handleSpace = (e) => {
      if (e.code === "Space") {
        e.preventDefault();
        setCount(0);
      }

Problem 2: Performance optimization on adding and removing event handlers.

Solution:

In the course, the event listeners are added and removed every time the components re-render due to a change in the state of the count. We can gain performance by using the useRef hook. It might be a bit of an advanced concept this early in the course. But maybe it's worth it to talk about the performance issue of the current approach, so people following along know that's an issue to avoid in real-world and will be addressed later in the course.

const countRef = useRef(count);

// Handling side-effects in hooks as best practices instead of mutating directly in the function body  
// Updating countRef.current whenever count changes
  useEffect(() => {
    countRef.current = count;
  }, [count]); // This useEffect is solely for keeping countRef.current in sync with count

Full Code

import { useEffect, useRef, useState } from "react";

import { ResetIcon, PlusIcon, MinusIcon } from "@radix-ui/react-icons";

import Title from "./Title";
import Count from "./Count";
import Button from "./Button";
import Card from "./Card";

function Counter() {
  const [count, setCount] = useState(0);

  const countRef = useRef(count);

  // Updating countRef.current whenever count changes
  useEffect(() => {
    countRef.current = count;
  }, [count]); // This useEffect is solely for keeping countRef.current in sync with count

  // Setting up and tearing down event listeners
  useEffect(() => {
    const handleArrowUp = (e) => {
      if (e.key === "ArrowUp") {
        e.preventDefault();
        setCount(countRef.current + 1);
      }
    };

    const handleArrowDown = (e) => {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        if (countRef.current === 0) return;
        setCount(countRef.current - 1);
      }
    };

    const handleSpace = (e) => {
      if (e.code === "Space") {
        e.preventDefault();
        setCount(0);
      }
    };

    window.addEventListener("keydown", handleArrowUp);
    window.addEventListener("keydown", handleArrowDown);
    window.addEventListener("keydown", handleSpace);

    return () => {
      window.removeEventListener("keydown", handleArrowUp);
      window.removeEventListener("keydown", handleArrowDown);
      window.removeEventListener("keydown", handleSpace);
    };
  }, []); // Note: No dependencies here, so this setup and teardown happens only once

  const handleIncrement = () => setCount(count + 1);
  const handleDecrement = () => setCount(count - 1);
  const handleReset = () => setCount(0);

  return (
    <Card>
      <Title title="fancy counter" />
      <Count count={count} />
      <Button
        theme={count === 0 ? "reset-btn-disabled" : "reset-btn"}
        onClick={handleReset}
        disabled={count === 0 ? true : false}
      >
        <ResetIcon className="reset-btn-icon" />
      </Button>
      <div className="button-container">
        <Button
          theme={count === 0 ? "count-btn-disabled" : "count-btn"}
          onClick={handleDecrement}
          disabled={count === 0 ? true : false}
        >
          <MinusIcon className="count-btn-icon" />
        </Button>
        <Button theme={"count-btn"} onClick={handleIncrement} disabled={false}>
          <PlusIcon className="count-btn-icon" />
        </Button>
      </div>
    </Card>
  );
}

export default Counter;

PS: I added arrow up and arrow down events for increment and decrement and also used a common pure Button component. I disabled the decrement and reset button when the count reaches zero. Below are the CSS styles I used -

.reset-btn-disabled {
  cursor: not-allowed;
  opacity: 0.3;
  transition: all 0.4s;
}

.reset-btn {
  cursor: pointer;
  opacity: 0.7;
  transition: all 0.4s;
}

.count-btn {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  transition: all 0.4s;
}

.count-btn-disabled {
  flex: 1;
  opacity: 0.3;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: not-allowed;
  transition: all 0.4s;
}

Button Component

function Button({ children, theme, onClick, disabled }) {
  return (
    <button className={theme} onClick={onClick} disabled={disabled}>
      {children}
    </button>
  );
}

export default Button;