skip to Main Content

I’ve built a simple stopwatch with start and stop buttons. I’ve also built a really barebones matching game where a user clicks on 5 different squares that start out as random colors (possibility of being 7 predefined colors). Every time the user clicks on a square, it changes colors, and when all 5 match, it renders a message on the screen and disables any clicking changes to the squares until you press a restart button (in which it gives you a new set of random colors). It also counts the number of times you’ve clicked.

I’m wanting to add in the timing element to the matching game. For now, what I’m looking to do is have a start button that starts the timer, but have the timer automatically stop once the condition of all 5 squares matching is met (so no stop button).

Where I’m struggling is how, and where, to move the logic to stop the timer from the stop button that I currently have, to where it should be for what I’m wanting to do. My best attempt so far was getting the entire timer to disappear when all 5 squares were matching, which is definitely not what I want. Or you know, I’ve just crashed the whole thing with too many re-renders.

Can anyone point me in the right direction of how I should be looking at this?

Matching Game

Some of the code below:

("GroupOfSquares" component where I have the majority of the logic for this game–receives "colorsArray" from app.js)

import Stopwatch from "./Stopwatch";
import Square from "./Square";
import { useState, useEffect } from "react";

function randomPick(array) {
  const index = Math.floor(Math.random() * array.length);
  return array[index];
}

export default function GroupOfSquares({ colorsArray }) {
  const genRandomColors = () => {
    let colorArrayCopy = [...colorsArray];
    let newArray = [];

    for (let i = 0; i < 5; i++) {
      let randNum = Math.floor(Math.random() * colorArrayCopy.length);
      let splicedItem = colorArrayCopy.splice(randNum, 1)[0]
      newArray.push(splicedItem);
    }
    return newArray;
  }

  genRandomColors(colorsArray);

  const [count, setCount] = useState(0);
  const [initialColors] = useState(genRandomColors);
  const [colors, setColors] = useState(initialColors);
  const [timerOn, setTimerOn] = useState(false);
  const [time, setTime] = useState(0);

  useEffect(() => {
    let interval = null;
    if (timerOn) {
      interval = setInterval(() => {
        setTime(prevTime => prevTime + 10)
      }, 10)
    } else {
      clearInterval(interval)
    }
    return () => clearInterval(interval);
  }, [timerOn])

  const squares = colors.map((color, i) => (
    <Square
      key={i}
      color={color}
      onClick={() => {
        setColors((prev) => {
          const next = [...prev];
          next[i] = randomPick(colorsArray);
          return next;
        });
        setCount((prev) => prev + 1);
      }}
    />
  ));

  const newSquares = squares.map((square) => {
    return square.props.color;
  });

  const allColorsEqual = function (newSquares) {
    return newSquares.every(val => val === newSquares[0]);
  }

  const isCompleted = allColorsEqual(newSquares);

  const reset = () => {
    setCount(0);
    setColors(genRandomColors);
    setTime(0);
  };

  return (
    <div className="Container">
      <Stopwatch
        Start={() => setTimerOn(true)}
        Stop={() => setTimerOn(false)}
        Time={time}
      />

      <div
        className="RowOfSquares" 
        style={{ pointerEvents: isCompleted ? 'none' : 'auto' }}
      >
        {squares}
      </div>
      {isCompleted && <h3>Matched!</h3>}
      <div className="clicks">
        <div className="numClicks">
          <span>{count} Clicks</span>
        </div>
        <button onClick={reset}>reset</button>
      </div>
    </div>
  );
}

Stopwatch component:

export default function Stopwatch({ Start, Time, Stop }) {
  return (
    <div>
      <div>
        <span>{("0" + Math.floor((Time / 60000) % 60)).slice(-2)}:</span>
        <span>{("0" + Math.floor((Time / 1000) % 60)).slice(-2)}:</span>
        <span>{("0" + ((Time / 10) % 100)).slice(-2)}</span>
      </div>
      <div className="buttons">
        <button onClick={Start}>start</button>
        <button onClick={Stop}>stop</button>
      </div>
    </div>
  );
}

2

Answers


  1. Write the logic to set timerOn state to false in a useState like below and make other adjustments related to this state if needed:

    useEffect(() => {
        const newSquares = colors.map((square) => square); // or just write structuredClone(square), just preventing destructuring since it creates a shallow copy
        const allColorsEqual = newSquares.every(val => val === newSquares[0]);
    
        if (allColorsEqual) {
            setTimerOn(false); // Stop the timer when all squares match
        }
    }, [colors]);
    
    Login or Signup to reply.
    • Create a state for the isCompleted value, compute the initial value from the initial colors state value

      const [isCompleted, setIsCompleted] = useState(
        () => allColorsEqual(colors)
      );
      
    • Use a useEffect hook to compute and update the isCompleted state when the colors state updates

      useEffect(() => {
        setIsCompleted(allColorsEqual(colors));
      }, [colors]);
      
    • Use a useEffect hook to end the timer when the isCompleted state is true

      useEffect(() => {
        if (isCompleted) {
          setTimerOn(false);
        }
      }, [isCompleted]);
      

    Full Component Example:

    const genRandomColors = (colorsArray) => {
      const colorArrayCopy = [...colorsArray];
      const newArray = [];
    
      for (let i = 0; i < 5; i++) {
        const randNum = Math.floor(Math.random() * colorArrayCopy.length);
        const [splicedItem] = colorArrayCopy.splice(randNum, 1);
        newArray.push(splicedItem);
      }
      return newArray;
    };
    
    const allColorsEqual = (colors) =>
      colors.every((color, i, arr) => color === arr[0]);
    
    export default function GroupOfSquares({ colorsArray }) {
      const [count, setCount] = useState(0);
      const [initialColors] = useState(() => genRandomColors(colorsArray));
      const [colors, setColors] = useState(initialColors);
      const [isCompleted, setIsCompleted] = useState(
        () => allColorsEqual(colors)
      );
      const [timerOn, setTimerOn] = useState(false);
      const [time, setTime] = useState(0);
    
      useEffect(() => {
        setIsCompleted(allColorsEqual(colors));
      }, [colors]);
    
      useEffect(() => {
        if (isCompleted) {
          setTimerOn(false);
        }
      }, [isCompleted]);
    
      useEffect(() => {
        let interval = null;
        if (timerOn) {
          interval = setInterval(() => {
            setTime((prevTime) => prevTime + 10);
          }, 10);
        } else {
          clearInterval(interval);
        }
        return () => clearInterval(interval);
      }, [timerOn]);
    
      const reset = () => {
        setCount(0);
        setColors(genRandomColors(colorsArray));
        setTime(0);
      };
    
      return (
        <div className="Container">
          <Stopwatch
            Start={() => setTimerOn(true)}
            Stop={() => setTimerOn(false)}
            Time={time}
          />
    
          <div
            className="RowOfSquares"
            style={{ pointerEvents: isCompleted ? "none" : "auto" }}
          >
            {colors.map((color, i) => (
              <Square
                key={i}
                color={color}
                onClick={() => {
                  setColors((colors) =>
                    colors.map((color, index) =>
                      index === i ? randomPick(colorsArray) : color
                    )
                  );
                  setCount((count) => count + 1);
                }}
              />
            ))}
          </div>
          {isCompleted && <h3>Matched!</h3>}
          <div className="clicks">
            <div className="numClicks">
              <span>{count} Clicks</span>
            </div>
            <button onClick={reset}>reset</button>
          </div>
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search