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
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.
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 singleref
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 youruseEffect
which containssquareRef
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 handlersquareRef.current.contains(e.target)
is only ever checking ife.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 providedrow
andcolumn
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.sandbox