简体   繁体   中英

Why is my state beeing updated before i call setState with React Hooks and how do i fix the disabled mouse pointer?

I have two problems at the moment with recreating a dijkstras pathfinding visualizer.

My Codesandbox: https://codesandbox.io/s/silent-morning-t84e0

  1. If you click/ click and drag onto the grid you can create wall-nodes that block the path. But it happens that, if you click and drag for a few nodes, release the mouse button and click and drag on the same node you ended on, the mouse pointer is somehow disabled and doesnt notice the onMouseUp event. result: the mouse is still clicked --> thus you still create walls onMouseOver even if the mouse is not pressed

  2. Previously the nodes that are visited by the algorithm were animated by adding a class via getElementById.classname . But i actually want to update the class in the child component by passing down the isVisited prop that is part of the state anyway. But i cant figure out why my isVisited in my state is updated before i call setState or how i can do it properly. Currently the all the visited nodes are animated at once before they go back to white as if they were not visited.

Wrapper Component:

import React, { useState, useEffect, useCallback, useRef } from "react";
import Node from "../Node/Node";

import "./PathfindingVisualizer.css";

import { dijkstra, getNodesInShortestPathOrder } from "../algorithms/dijkstras";

const START_NODE_ROW = 0;
const START_NODE_COL = 0;
const FINISH_NODE_ROW = 0;
const FINISH_NODE_COL = 3;

const TOTAL_ROWS = 5;
const TOTAL_COLS = 10;

const PathfindingVisualizer = () => {
  const [nodeGrid, setNodeGrid] = useState({
    grid: []
  });

  const mouseIsPressed = useRef(false);

  useEffect(() => {
    const grid1 = getInitialGrid();
    setNodeGrid({ ...nodeGrid, grid: grid1 });
  }, []);

  const handleMouseDown = useCallback((row, col) => {
    //console.log(newGrid);
    setNodeGrid(prevGrid => ({
      grid: getNewGridWithWallToggled(prevGrid.grid, row, col)
    }));
    mouseIsPressed.current = true;
    //console.log(nodeGrid);
  }, []);

  // function handleMouseDown(row, col) {
  //   const newGrid = getNewGridWithWallToggled(nodeGrid.grid, row, col);
  //  console.log(newGrid);
  //   setNodeGrid({...nodeGrid, nodeGrid[row][col]= newGrid});
  // }

  const handleMouseEnter = useCallback((row, col) => {
    //console.log(mouseIsPressed);
    if (mouseIsPressed.current) {
      setNodeGrid(prevNodeGrid => ({
        ...prevNodeGrid,
        grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
      }));
    }
  }, []);

  const handleMouseUp = useCallback(() => {
    mouseIsPressed.current = false;
  }, []);

  // const animateDijkstra = (visitedNodesInOrder, nodesInShortestPathOrder) => {
  //   for (let i = 0; i <= visitedNodesInOrder.length; i++) {
  //     if (i === visitedNodesInOrder.length) {
  //       setTimeout(() => {
  //         animateShortestPath(nodesInShortestPathOrder);
  //       }, 10 * i);
  //       return;
  //     }
  //     setTimeout(() => {
  //       const node = visitedNodesInOrder[i];
  //       document.getElementById(`node-${node.row}-${node.col}`).className =
  //         "node node-visited";
  //     }, 10 * i);
  //   }
  // };

  const animateDijkstra = (visitedNodesInOrder, nodesInShortestPathOrder) => {
    for (let i = 0; i <= visitedNodesInOrder.length; i++) {
      if (i === visitedNodesInOrder.length) {
        setTimeout(() => {
          animateShortestPath(nodesInShortestPathOrder);
        }, 17 * i);
        return;
      }
      setTimeout(() => {
        const node = visitedNodesInOrder[i];
        console.log("node", node);
        console.log("state", nodeGrid);
        console.log(
          "before setNode",
          nodeGrid.grid[node.row][node.col].isVisited
        );
        setNodeGrid(prevNodeGrid => ({
          ...prevNodeGrid,
          grid: getNewGridWithVisited(prevNodeGrid.grid, node.row, node.col)
          //   //grid: node
        }));
        console.log(
          "after setNode;",
          nodeGrid.grid[node.row][node.col].isVisited
        );
      }, 17 * i);
    }
  };

  const animateShortestPath = nodesInShortestPathOrder => {
    for (let i = 0; i < nodesInShortestPathOrder.length; i++) {
      setTimeout(() => {
        const node = nodesInShortestPathOrder[i];
        document.getElementById(`node-${node.row}-${node.col}`).className =
          "node node-shortest-path";
      }, 50 * i);
    }
  };

  const visualizeDijkstra = () => {
    const grid = nodeGrid.grid;
    console.log(grid);
    const startNode = grid[START_NODE_ROW][START_NODE_COL];
    const finishNode = grid[FINISH_NODE_ROW][FINISH_NODE_COL];
    const visitedNodesInOrder = dijkstra(grid, startNode, finishNode);
    const nodesInShortestPathOrder = getNodesInShortestPathOrder(finishNode);
    animateDijkstra(visitedNodesInOrder, nodesInShortestPathOrder);
  };

  //console.log(nodeGrid.grid);
  //console.log(visualizeDijkstra());
  return (
    <>
      <button onClick={visualizeDijkstra}>
        Visualize Dijkstra´s Algorithm
      </button>
      <div className="grid">
        test
        {nodeGrid.grid.map((row, rowIdx) => {
          return (
            <div className="row" key={rowIdx}>
              {row.map((node, nodeIdx) => {
                const { row, col, isStart, isFinish, isWall, isVisited } = node;
                return (
                  <Node
                    key={nodeIdx}
                    col={col}
                    row={row}
                    isStart={isStart}
                    isFinish={isFinish}
                    isWall={isWall}
                    isVisited={isVisited}
                    onMouseDown={handleMouseDown}
                    onMouseEnter={handleMouseEnter}
                    onMouseUp={handleMouseUp}
                  />
                );
              })}
            </div>
          );
        })}
      </div>
    </>
  );
};

export default PathfindingVisualizer;

//----------------------------------------------------------

const getInitialGrid = () => {
  const grid = [];
  for (let row = 0; row < TOTAL_ROWS; row++) {
    const currentRow = [];
    for (let col = 0; col < TOTAL_COLS; col++) {
      currentRow.push(createNode(col, row));
    }
    grid.push(currentRow);
  }
  return grid;
};

const createNode = (col, row) => {
  return {
    col,
    row,
    isStart: row === START_NODE_ROW && col === START_NODE_COL,
    isFinish: row === FINISH_NODE_ROW && col === FINISH_NODE_COL,
    distance: Infinity,
    isVisited: false,
    isWall: false,
    previousNode: null
  };
};

const getNewGridWithWallToggled = (grid, row, col) => {
  const newGrid = grid.slice();
  const node = newGrid[row][col];
  const newNode = {
    ...node,
    isWall: !node.isWall
  };
  newGrid[row][col] = newNode;
  return newGrid;
};

const getNewGridWithVisited = (grid, row, col) => {
  const newGrid = grid.slice();
  const node1 = newGrid[row][col];
  const newNode = {
    ...node1,
    isVisited: true
    //isVisited: !node1.isVisited
  };
  //console.log(newNode);
  newGrid[row][col] = newNode;
  return newGrid;
};

Child Component:

import React from "react";

import "./Node.css";
import { useCountRenders } from "../Node/useCountRenders";

const Node = React.memo(
  ({
    col,
    isFinish,
    isStart,
    isWall,
    onMouseDown,
    onMouseEnter,
    onMouseUp,
    row,
    isVisited
  }) => {
    //console.log("props: col, isWall, row;", col, isWall, row);
    const extraClassName = isFinish
      ? "node-finish"
      : isStart
      ? "node-start"
      : isWall
      ? "node-wall"
      : isVisited
      ? 'node-visited'
      : "";

    useCountRenders();

    console.log("node rerendered: row:", row, "col:", col);
    return (
      <div
        id={`node-${row}-${col}`}
        className={`node ${extraClassName}`}
        onMouseDown={() => onMouseDown(row, col)}
        onMouseEnter={() => onMouseEnter(row, col)}
        onMouseUp={() => onMouseUp()}
      />
    );
  }
);

export default Node;

I am not sure I can reproduce the exact same bug that you explain. But right now your code is maybe not handling correctly the mouse up and down event.

What I could reproduce: you click drag and mouse up outside of the node. If you bring back the mouse in the node it will act like the mouse is still down.

If you want to fix that, rather than listen on each separate node for mouse up, you can use an useEffect(() => { with window.addEventListener("mouseup"

For your first problem:-

Do a e.preventDefault() in your handleMouseDown code like so:-

const handleMouseDown = useCallback((e, row, col) => { //your logic e.preventDefault(); }, []);

And in your Node.jsx:-

onMouseDown={(e) => onMouseDown(e, row, col)}

For the second problem:-

I think I found the root cause of it. Basically you're passing grid object to dijkstra's function. You are using it in your whole algorithm without making a copy of it first (Remember objects are passed as references in JS). That's why isVisited becomes true because your algorithm has modified grid. So inside, dijkstra's make a deep copy of grid and then start with your algo. I hope this helps:).

From an immediate glance without going through the code in depth it looks like all of your handlers use empty dependency arrays eg

  const handleMouseEnter = useCallback((row, col) => {
    //console.log(mouseIsPressed);
    if (mouseIsPressed.current) {
      setNodeGrid(prevNodeGrid => ({
        ...prevNodeGrid,
        grid: getNewGridWithWallToggled(prevNodeGrid.grid, row, col)
      }));
    }
  }, []); <= empty dependency array

Meaning it will only ever re-render the function on component mount. I'm assuming you probably don't want that on something like a handleMouseEnter as subsequent enters it will have stale values from previous render. The dependency array should have any values the function needs depends on such as prevNodeGrid for example.

Same possibly goes for some of your useEffects unless you only want them running once on component mount.

Because the grid is an array that contains other arrays, a deep copy is required if it is passed to a function and modified in the process. Otherwise changes that are made to the nested data will also change the original object/array that was passed.

I used lodash to to create a deep-copy of the nodeGrid.grid -state before it is passed to the dijkstras function.

import _ from "lodash";

//--------

 const visualizeDijkstra = () => {

    const grid = _.cloneDeep(nodeGrid.grid);

    // other code
  }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM