I am developing an application in react typescript environment and I faced a problem which I cannot resolve on my own. Whenever I try to set state to its initial value I get my UI updated although console.log(state) would still return previous state. When I console.log it using useEffect with dependency to my state I get the updated value printed out.
My biggest concern is how can that happen that react 'saves' previous state somewhere and uses it when user is performing a drag and drop interaction.
const [board, setBoard] = useState(initialState);
function clearBoard(){
updateBoard(initialState)
console.log(board)
}
function updateBoard(board: UnitHex[][]){
setBoard(board);
//some side actions
}
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
function dragStart(event: any) {
event.dataTransfer.setData("text", event.target.id);
}
function dragOver(event: any) {
event.preventDefault();
}
function drop(event: any) {
event.preventDefault();
var tempBoard: UnitHex[][] = board;
console.log("inside drop fn")
console.log(tempBoard);
//drop logic
updateBoard(tempBoard);
event.stopImmediatePropagation();
}
useEffect(() => {
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
console.log(board)
}, [board]);
Does anyone know what's going on? Thanks in advance!!
CodePudding user response:
It's not React that's storing the old information, it's your code. :-)
You're getting elements directly from the DOM during the main run of your component function:
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
...which is not how you should do things in React. Then you're adding event handlers to those elements:
useEffect(() => {
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
console.log(board);
}, [board]);
...and never removing old handlers. So the elements keep the old handler and the new one. The old handler runs first, does event.stopImmediatePropagation()
, and that prevents the new handler from running.
At a minimum:
- Don't grab the elements during render, do it in an effect afterward
- Remove old handlers with a cleanup callback
Like this:
useEffect(() => {
const draggableElements = document.querySelectorAll(".draggable");
const droppableElements = document.querySelectorAll(".droppable");
droppableElements.forEach((element) => {
element.addEventListener("dragover", dragOver);
element.addEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.addEventListener("dragstart", dragStart);
});
return () => { // Cleanup callback
droppableElements.forEach((element) => {
element.removeEventListener("dragover", dragOver);
element.removeEventListener("drop", drop);
});
draggableElements.forEach((element) => {
element.removeEventListener("dragstart", dragStart);
});
};
}, [board]);
That way, only the up-to-date version of dragOver
and drop
will be hooked up on the elements.
But again, using DOM methods like document.querySelectorAll
is usually not best practice in React code. There are rare exceptions; drag and drop may be one of them, although handling it at the component level with global elements seems a bit off.