Skip to content
Advertisement

How to stop certain react components from rerendering after the state of another component changes

So I want to render x amount of circles. Every circle is rendered on a random position on the screen. Each one of them has a value parameter, which is the amount that will be added to the total point count displayed in the top left corner. After clicking a circle, it should add its value to the count and disappear. It does what it should, however on click, all the other circles get rerendered with a new random position, which they shouldn’t. The initially set x and y position should be generated once for each circle and stay like that. How do I prevent that from happening? Here is my code:

import { useState } from "react";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);
  let ctr = 0;

  function getRandom(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
  }

  function renderCircle(xPos, yPos, color, value) {
    ctr++;

    const handleClick = (id) => () => {
      setCount(count + value);
      const el = document.getElementById(id);
      el.style.visibility = "hidden";
    };
    return (
      <div>
        {
          <svg
            className="circle"
            id={ctr}
            onClick={handleClick(ctr)}
            style={{
              left: `calc(${xPos}vw - 60px)`,
              top: `calc(${yPos}vw - 60px)`,
              position: "absolute",
            }}
          >
            <circle cx={30} cy={30} r={30} fill={color} />
          </svg>
        }
      </div>
    );
  }

  function renderCircles(amount) {
    const arr = [];
    for (let i = 0; i < amount; i++) {
      let circle = renderCircle(
        getRandom(3, 53),
        getRandom(3, 40),
        Object.keys(Color)[
          Math.floor(Math.random() * Object.keys(Color).length)
        ],
        getRandom(1, 100)
      );
      arr.push(circle);
    }
    return arr;
  }

  return (
    <div className="App">
      <div className="App-game">{renderCircles(15)}</div>
      <div className='App-currency' style={{color: "black", fontSize: 80}}>
        {count}
      </div>
    </div>
  );
}

export default App;

class Color {
  static Green = new Color("green");
  static Blue = new Color("blue");
  static Yellow = new Color("yellow");
  static Red = new Color("red");
  static Pink = new Color("pink");
  static Orange = new Color("orange");

  constructor(name) {
    this.name = name;
  }
  toString() {
    return `Color.${this.name}`;
  }
}

Advertisement

Answer

You should hold the circles in a state variable

  const [circles] = useState(renderCircles(15));

  return (
    <div className="App">
      <div className="App-game">{circles}</div>
      <div className="App-currency" style={{ color: 'black', fontSize: 80 }}>
        {count}
      </div>
    </div>
  );

You also have an issue with the size of the svg wrapping the circles – it’s much bigger, so the click event can be fired without actually clicking on the circle. You need to set the display to none to prevent the circle from being clicked on again as well.

  function renderCircle(xPos, yPos, color, value) {
    const id = (ctr++).toString();
    function handleClick() {
      setCount((prev) => prev + value);
      const el = document.getElementById(id);
      el.style.display = 'none';
    }
    return (
      <svg
        className="circle"
        id={id}
        onClick={handleClick}
        style={{
          left: `calc(${xPos}vw - 60px)`,
          top: `calc(${yPos}vw - 60px)`,
          width: 60,
          height: 60,
          position: 'absolute',
        }}
      >
        <circle cx={'50%'} cy={'50%'} r={'50%'} fill={color} />
      </svg>
    );
  }

Stackblitz: https://stackblitz.com/edit/react-ts-b6jddu?file=App.tsx


Notice when incrementing the count I also changed the setCount parameter to a callback function. You should make a habit of doing that when the new value relies on the previous value. Otherwise you may run into bugs down the line.

For example:

const {useState} = React;

function App() {
  // count is a local variable, which the value of the state gets copied onto here
  const [count, setCount] = useState(0);
  
  function incrementThreeTimesWrong() {
    // count is 0 here
    setCount(count + 1); // sets to 0 + 1
    setCount(count + 1); // sets to 0 + 1
    setCount(count + 1); // sets to 0 + 1
    // count is still 0 here - setCount does not mutate the local variable
    // However setCount triggers a rerender, which will update the local variable to 1
  }

  function incrementThreeTimesRight() {
    setCount(prev => prev + 1); // sets to 0 + 1
    setCount(prev => prev + 1); // sets to 1 + 1
    setCount(prev => prev + 1); // sets to 2 + 1
  }
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={incrementThreeTimesWrong}>Increment three times the wrong way</button>
      <br/>
      <button onClick={incrementThreeTimesRight}>Increment three times the right way</button>
    </div>);
}

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement