javascript - React Hooks (Rendering Arrays) - 持有被映射的 child 的引用的父组件与持有 child 状态的父组件

标签 javascript reactjs react-hooks

在过去的几天里,我一直在学习 react 中的钩子(Hook),我尝试创建一个场景,我需要在屏幕上渲染一个大网格,并根据我想要采取的操作更新节点的背景颜色。有两个 Action 会改变节点的背景颜色,这两个 Action 必须同时存在。

  • 单击时光标悬停在节点上。
  • Grid 组件内部存在一个算法,它会改变某些元素的背景
    节点。

  • 在我看来,有多种方法可以实现这一点,但我在使用钩子(Hook)的方式上遇到了一些麻烦。我将首先引导您了解如何从我学到的知识中实现这一点的思考过程,然后向您展示我尝试过的实现。我试图保留代码的重要部分,以便可以清楚地理解。如果我错过了什么或完全误解了一个概念,请告诉我。
  • children 可以保持自己的状态并知道如何更新自己。父级可以保存对列表中每个子级的引用,并在需要时从子级的引用中调用必要的函数以更新子级。
  • 适用于要采取的第一个和第二个 Action 。此解决方案不会导致性能问题,因为子级管理自己的状态,并且如果父级通过引用更新子级状态,则唯一要重新渲染的子级将是被调用的那个。
  • 从我阅读的内容来看,这个解决方案被视为一种反模式。
  •     const Grid = () => {
            // grid array contains references to the GridNode's
    
            function handleMouseDown() {
                setIsMouseDown(true);
            }
    
            function handleMouseUp() {
                setIsMouseDown(false);
            }
    
            function startAlgorithm() {
                // call grid[row][column].current.markAsVisited(); for some of the children in grid.
            }
    
            return (
                <table>
                    <tbody>
                    {
                        grid.map((row, rowIndex) => {
                                return (
                                    <tr key={`R${rowIndex}`}>
                                        {
                                            row.map((node, columnIndex) => {
                                                return (
                                                    <GridNode
                                                        key={`R${rowIndex}C${columnIndex}`}
                                                        row={rowIndex}
                                                        column={columnIndex}
                                                        ref={grid[rowIndex][nodeIndex]}
                                                        onMouseDown={handleMouseDown}
                                                        onMouseUp={handleMouseUp}
                                                    />
                                                );
                                            })
                                        }
                                    </tr>
                                );
                            }
                        )
                    }
                    </tbody>
                </table>
            );
        };
    
        const GridNode = forwardRef((props, ref) => {
            const [isVisited, setIsVisited] = useState(false);
    
            useImperativeHandle(ref, () => ({
                markAsVisited: () => {
                    setIsVisited(!isVisited);
                }
            }));
    
            function handleMouseDown(){
                    setIsVisited(!isVisited);
                }
    
            function handleMouseEnter () {
                    if (props.isMouseDown.current) {
                        setIsVisited(!isVisited);
                    }
                }
    
            return (
                <td id={`R${props.row}C${props.column}`}
                    onMouseDown={handleMouseDown}
                    onMouseEnter={handleMouseEnter}
                    className={classnames("node", {
                        "node-visited": isVisited
                    })}
                />
            );
        });
    
    

    2. 子节点的状态可以作为父节点的props,任何更新操作都可以在父节点内部实现。 (子节点被正确更新,render 仅在必要的子节点中被调用,但 DOM 似乎结结巴巴。如果您以一定的速度移动鼠标,则什么也不会发生,并且每个访问的节点都会立即更新。)
  • 不适用于第一个 Action 。子项得到正确更新,渲染只在必要的子项中被调用,但 DOM 似乎结结巴巴。如果您以一定的速度移动鼠标,则不会发生任何事情,并且每个访问的节点都会立即更新。
  •     const Grid = () => {
            // grid contains objects that have boolean "isVisited" as a property.
    
            function handleMouseDown() {
                isMouseDown.current = true;
            }
    
            function handleMouseUp() {
                isMouseDown.current = false;
            }
    
            const handleMouseEnterForNodes = useCallback((row, column) => {
                if (isMouseDown.current) {
                    setGrid((grid) => {
                        const copyGrid = [...grid];
    
                        copyGrid[row][column].isVisited = !copyGrid[row][column].isVisited;
    
                        return copyGrid;
                    });
                }
            }, []);
    
            function startAlgorithm() {
                // do something with the grid, update some of the "isVisited" properties.
    
                setGrid(grid);
            }
    
            return (
                <table>
                    <tbody>
                    {
                        grid.map((row, rowIndex) => {
                                return (
                                    <tr key={`R${rowIndex}`}>
                                        {
                                            row.map((node, columnIndex) => {
                                                const {isVisited} = node;
    
                                                return (
                                                    <GridNode
                                                        key={`R${rowIndex}C${columnIndex}`}
                                                        row={rowIndex}
                                                        column={columnIndex}
                                                        isVisited={isVisited}
                                                        onMouseDown={handleMouseDown}
                                                        onMouseUp={handleMouseUp}
                                                        onMouseEnter={handleMouseEnterForNodes}
                                                    />
                                                );
                                            })
                                        }
                                    </tr>
                                );
                            }
                        )
                    }
                    </tbody>
                </table>
            );
        };
    
        const GridNode = ({row, column, isVisited, onMouseUp, onMouseDown, onMouseEnter}) => {
            return useMemo(() => {
                function handleMouseEnter() {
                    onMouseEnter(props.row, props.column);
                }
    
                return (
                    <td id={`R${row}C${column}`}
                        onMouseEnter={handleMouseEnter}
                        onMouseDown={onMouseDown}
                        onMouseUp={onMouseUp}
                        className={classnames("node", {
                            "node-visited": isVisited
                        })}
                    />
                );
            }, [props.isVisited]);
        }
    

    关于这个话题,我有两个问题想问。
  • 在第一个实现中;当节点改变其状态时,父组件不会重新渲染。如果在这种情况下有益,那么仅仅使用这种反模式是错误的吗?
  • 第二种实现遇到的口吃可能是什么原因?我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。
  • 最佳答案

    正如您所说,使用 refs 控制子数据是一种反模式,但这并不意味着您不能使用它。

    这意味着如果有更好和更高性能的方法,最好使用它们,因为它们可以提高代码的可读性并改善调试。

    在您的情况下,使用 ref 绝对可以更轻松地更新状态并且还可以防止大量重新渲染是实现上述解决方案的好方法

    第二种实现遇到的口吃可能是什么原因?我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。

    第二种解决方案中的许多问题都源于您定义了在每次重新渲染时重新创建的函数,因此导致整个网格被重新渲染,而不仅仅是单元格。在 Grid 组件中使用 useCallback 来内存这些函数

    你也应该使用 React.memo而不是 useMemo用于 GridNode 中的用例。

    另一件需要注意的是,您在更新时正在改变状态,相反,您应该以不可变的方式更新它

    工作代码:

    const Grid = () => {
      const [grid, setGrid] = useState(getInitialGrid(10, 10));
      const isMouseDown = useRef(false);
      const handleMouseDown = useCallback(() => {
        isMouseDown.current = true;
      }, []);
    
      const handleMouseUp = useCallback(() => {
        isMouseDown.current = false;
      }, []);
    
      const handleMouseEnterForNodes = useCallback((row, column) => {
        if (isMouseDown.current) {
          setGrid(grid => {
            return grid.map((r, i) =>
              r.map((c, ci) => {
                if (i === row && ci === column)
                  return {
                    isVisited: !c.isVisited
                  };
                return c;
              })
            );
          });
        }
      }, []);
    
      function startAlgorithm() {
        // do something with the grid, update some of the "isVisited" properties.
    
        setGrid(grid);
      }
    
      return (
        <table>
          <tbody>
            {grid.map((row, rowIndex) => {
              return (
                <tr key={`R${rowIndex}`}>
                  {row.map((node, columnIndex) => {
                    const { isVisited } = node;
                    if (isVisited === true) console.log(rowIndex, columnIndex);
                    return (
                      <GridNode
                        key={`R${rowIndex}C${columnIndex}`}
                        row={rowIndex}
                        column={columnIndex}
                        isVisited={isVisited}
                        onMouseDown={handleMouseDown}
                        onMouseUp={handleMouseUp}
                        onMouseEnter={handleMouseEnterForNodes}
                      />
                    );
                  })}
                </tr>
              );
            })}
          </tbody>
        </table>
      );
    };
    
    const GridNode = ({
      row,
      column,
      isVisited,
      onMouseUp,
      onMouseDown,
      onMouseEnter
    }) => {
      function handleMouseEnter() {
        onMouseEnter(row, column);
      }
      const nodeVisited = isVisited ? "node-visited" : "";
      return (
        <td
          id={`R${row}C${column}`}
          onMouseEnter={handleMouseEnter}
          onMouseDown={onMouseDown}
          onMouseUp={onMouseUp}
          className={`node ${nodeVisited}`}
        />
      );
    };
    

    Edit FORM VALUES

    附:而useCallback和其他内存将有助于提供一些性能优势,但仍然无法克服对状态更新和重新渲染的性能影响。在这种情况下,最好在子级中定义状态并为父级公开一个 ref

    关于javascript - React Hooks (Rendering Arrays) - 持有被映射的 child 的引用的父组件与持有 child 状态的父组件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62292736/

    相关文章:

    javascript - 当我点击 ok swal 按钮时如何重新加载?

    javascript - 如何使此横幅 slider 适合浏览器高度?

    javascript - jQuery:不活动刷新/注销

    reactjs - 从嵌套的 StackNavigator 中隐藏 TabBar 的 react 导航屏幕

    javascript - POST 数据未以正确的格式从 Angular 2 服务发送,但从 PostMan 发送时工作正常

    jquery - 在react componentDidMount中bind(this)后获取jquery对象

    reactjs - className 属性是否承担 Reactjs 中 id 属性的角色?

    javascript - 如何从 useEffect 访问组件的当前状态而不使其成为依赖项?

    javascript - React hooks,每个状态值一个钩子(Hook)?

    javascript - 如何使用钩子(Hook)在 React 中使用 setTimeout() 来实现加载状态?