skip to Main Content

I’m trying to select squares and display which square number is selected. As you can see in my code I’m trying to unselect by clicking outside of it, and it works. The problem here is that if a have a selected square and click another square the text that appears at the top momentarily disappears and seems odd, I don’t want that to happen, I just want the text to change if have one square selected and click another one, but keeping the unselect property when clicking outside a square.

const { useState, useEffect, useRef } = React;

const App = () => {
  const [selectedSquare, setSelectedSquare] = useState(null);

  const squareRef = useRef(null);

  const handleSquareClick = (squareIndex) => {
    setSelectedSquare(squareIndex);
  };


  useEffect(() => {
    const handler = (e) => {
        if (squareRef.current && !squareRef.current.contains(e.target)) {
          setSelectedSquare(null);
        }
    };
    document.addEventListener("mousedown",handler)

    return()=>{
        document.removeEventListener("mousedown",handler)
    }
}, [squareRef])

  return (
    <div>
      <style>
        {`
        .square-row {
          display: flex;
        }
        
        .square {
          width: 100px;
          height: 100px;
          background-color: #ccc;
          margin-right: 10px;
          margin-top: 10px;
          cursor: pointer;
        }
        
        .selected {
          background-color: orange;
        }
        `}
      </style>
      {selectedSquare && <div>Selected Square: {selectedSquare}</div>}
      <div className="square-row">
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 1 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(1)}
        ></div>
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 2 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(2)}
        ></div>
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 3 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(3)}
        ></div>
      </div>
      <div className="square-row">
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 4 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(4)}
        ></div>
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 5 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(5)}
        ></div>
        <div
          ref={squareRef}
          className={`square ${selectedSquare === 6 ? 'selected' : ''}`}
          onClick={() => handleSquareClick(6)}
        ></div>
      </div>
    </div>
  );
};

ReactDOM.createRoot(document.body).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

This is what I’ve done so far, but this small bug keeps annoying me, I don’t know if it is because of the useEffect, but I want it to go and I haven’t been able to find a solution.

2

Answers


  1. Chosen as BEST ANSWER

    The solution given by ray was to stop the propagation of the event in the square click handler and change the event from mousedown to click. This works.

    import React, { useState, useEffect, useRef } from 'react';
    
    const App = () => {
      const [selectedSquare, setSelectedSquare] = useState(null);
    
      const squareRef = useRef(null);
    
      const handleSquareClick = (squareIndex, event) => {
        event.stopPropagation();
        setSelectedSquare(squareIndex);
      };
    
    
      useEffect(() => {
        const handler = (e) => {
            if (squareRef.current && !squareRef.current.contains(e.target)) {
              setSelectedSquare(null);
            }
        };
        document.addEventListener("click",handler)
    
        return()=>{
            document.removeEventListener("click",handler)
        }
    }, [squareRef])
    
      return (
        <div>
          <style>
            {`
            .square-row {
              display: flex;
            }
            
            .square {
              width: 100px;
              height: 100px;
              background-color: #ccc;
              margin-right: 10px;
              margin-top: 10px;
              cursor: pointer;
            }
            
            .selected {
              background-color: orange;
            }
            `}
          </style>
          {selectedSquare && <div>Selected Square: {selectedSquare}</div>}
          <div className="square-row">
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 1 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(1, event)}
            ></div>
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 2 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(2, event)}
            ></div>
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 3 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(3, event)}
            ></div>
          </div>
          <div className="square-row">
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 4 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(4, event)}
            ></div>
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 5 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(5, event)}
            ></div>
            <div
              ref={squareRef}
              className={`square ${selectedSquare === 6 ? 'selected' : ''}`}
              onClick={(event) => handleSquareClick(6, event)}
            ></div>
          </div>
        </div>
      );
    };
    
    export default App;
    

  2. While the propagation ‘fix’ may work, it is glossing over some larger issues with your code. First of all your use of useRef is not correct in that it simply overwrites a single ref with each successive element that you you pass to it resulting in its final assigned value being the last ‘square’ div. Which brings us to the dependency array of your useEffect which contains squareRef which by definition will never change (or it wouldn’t be a ref) so you are effectively passing an empty dependency array. Finally your condition in your document level handler squareRef.current.contains(e.target) is only ever checking if e.target contains the last square div due to the assignment issues noted above.

    Adding a document level handler to track outside clicks is fine, but it can be much simpler than your current implementation. Here is an example that uses nested Array.from() calls to generate the grid from provided row and column props which avoids duplication. It uses simple event delegation in the document level handler to identify clicks inside a square, !e.target.classList.contains("square") and it adds and cleans up the document level listener on component mount/dismount using an empty dependency array.

    const { useState, useEffect } = React;
    
    const Grid = ({ rows, columns }) => {
      const [selectedSquare, setSelectedSquare] = useState(null);
    
      const handleSquareClick = (squareIndex) => {
        setSelectedSquare(squareIndex);
      };
    
      useEffect(() => {
        const handler = (e) => {
          if (!e.target.classList.contains("square")) {
            setSelectedSquare(null);
          }
        };
    
        document.addEventListener("click", handler);
    
        return () => {
          document.removeEventListener("click", handler);
        };
      }, []);
    
      return (
        <div>
          {selectedSquare && <div>Selected Square: {selectedSquare}</div>}
          {Array.from({ length: rows }, (_, row) => (
            <div key={row} className="square-row">
              {Array.from({ length: columns }, (_, square) => {
                const squareNumber = row * columns + square + 1;
    
                return (
                  <div
                    key={`${row}_${square}`}
                    className={`square ${
                      selectedSquare === squareNumber ? "selected" : ""
                    }`}
                    onClick={() => handleSquareClick(squareNumber)}
                  >
                    {squareNumber}
                  </div>
                );
              })}
            </div>
          ))}
        </div>
      );
    };
    
    ReactDOM.createRoot(document.body).render(<Grid rows={2} columns={3} />);
    .square-row {
        display: flex;
    }
    
    .square {
        width: 100px;
        height: 100px;
        background-color: #ccc;
        margin-right: 10px;
        margin-top: 10px;
        cursor: pointer;
    
        display: grid;
        place-items: center;
        font: 2em sans-serif;
        color: tomato;
    }
    
    .selected {
        background-color: orange;
        color: dodgerblue;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

    sandbox

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search