繁体   English   中英

React Hooks (Rendering Arrays) - 父组件持有映射的子组件的引用与父组件持有 state 的子组件

[英]React Hooks (Rendering Arrays) - Parent component holding a reference of children that are mapped vs Parent component holding the state of children

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

  • cursor 在单击节点时将其悬停。
  • Grid组件内部存在一种算法,可以改变一些节点的背景。

在我看来,有多种方法可以实现这一点,但我在使用钩子的方式上遇到了一些麻烦。 我将首先引导您了解如何从我学到的知识中实现这一点的思考过程,然后向您展示我尝试过的实现。 我试图保留代码的重要部分,以便可以清楚地理解。 如果我错过了什么或完全误解了一个概念,请告诉我。

  1. 孩子们可以拿着自己的state,知道如何更新自己。 父级可以持有对列表中每个子级的引用,并在需要更新子级时从子级的引用中调用必要的function。

    • 适用于要采取的第一个和第二个动作。 此解决方案不会导致性能问题,因为孩子管理自己的 state,如果父通过引用更新孩子 state,则唯一要重新渲染的孩子将是被调用的孩子。
    • 从我阅读的内容来看,这个解决方案被视为一种反模式。

    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、children的state可以作为来自parent的props,任何更新操作都可以在parent内部实现。 (子节点被正确更新,render 仅在必要的子节点中被调用,但 DOM 似乎结结巴巴。如果您以一定的速度移动鼠标,则什么也不会发生,并且每个访问的节点都会立即更新。)

  • 不适用于第一个动作。 子项得到正确更新,渲染只在必要的子项中被调用,但 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]);
    }


关于这个话题,我有两个问题想问。

  1. 在第一个实现中; 当节点更改其 state 时,父组件不会重新渲染。 如果在这种情况下有益,那么仅仅使用这种反模式是错误的吗?

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

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

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

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

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

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

此外,您应该在useMemo中为您的用例使用React.memo而不是 useMemo。

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

工作代码:

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}`}
    />
  );
};

编辑表格值

PS 虽然useCallback和其他记忆将有助于提供一些性能优势,但它仍然无法克服对 state 更新和重新渲染的性能影响。 在这种情况下,最好在子级中定义 state 并为父级公开一个参考

如前所述,该解决方案是一种反模式,因为您在两个级别上混合了呈现和业务逻辑。 您不需要显式使用React.forwardRef ,实际上根据您不应该使用的文档,即使在编写 HOC(高阶组件)时也是如此。 你不需要直接访问元素并对其执行某种操作——让 React 做它的事情。 这是非常好的和有效的。

通常,当您在有 n 个节点的子节点树上调用重新渲染方法时,您不希望从顶级节点(在这种情况下为父节点)重新渲染,因为它会导致整个节点树重新渲染成一个新的 DOM 元素,而不是更新现有元素。

您当前的解决方案结合了父触发渲染和子触发渲染。 React 页面有一个很好的例子,带有井字游戏应用程序,说明如何在不导致父级重新渲染的情况下渲染子级。

您应该使用的策略是父节点具有 object 结构,在这种情况下为 n^2 个节点(例如,10x10 用于 arguments),是将渲染功能传递给子节点,并让子节点处理渲染。

当您从父节点触发渲染时,您有几个选项(假设是功能组件),它们确实属于可观察更新的情况。 您希望能够将更新从父节点推送到子节点,以修改子节点 state,并让子节点自行更新。

这是一个子节点渲染的示例,而父节点正在将更改传达给子节点。 与示例所具有的嵌套级别渲染相比,您会看到性能甚至可以扩展到大型网格。

https://codepen.io/jmitchell38488/pen/pogbKEb

这是通过使用 RxJS observable/subject、 React.useStateReact.useEffect的组合来实现的。 我们在父节点和子节点中都使用useState来处理渲染和道具更新,并useEffect来绑定 observable。 useState在渲染之间是持久的,这意味着您不需要每次在父节点中更新时都重建整个网格,但即使这样做,React 也足够智能,可以确定您更新了节点的 props,而不是替换它.

const Grid = (props) => {
  // When we update the grid, we trigger the parent to re-render
  const [grid, setGrid] = React.useState([]);
  const subject = new Rx.Subject();
  if (grid.length < 1) {
    const newGrid = [];
    for (i = 0; i < props.h; i++) {
      for (k = 0; k < props.w; k++) {
        if (!Array.isArray(newGrid[i])) {
          newGrid[i] = [];
        }

        newGrid[i][k] = {
          visited: false,
          id: `${i}${k}`
        };
      }
    }
    setGrid(newGrid);
  }

  // Tell our node to update
  handleClick = (node, visited) => {
    subject.next({
      id: node.id,
      visited: visited
    })
  };

  randomSetAllVisited = () => {
    const newGrid = [...grid];
    newGrid.forEach(row => {
      row.forEach(node => {
        node.visited = Math.random() * 2 >= 1;
      })
    })

    // Tell parent to re-render
    setGrid(newGrid);

    // Because our nodes use `useState`, they are persistent, if the structure of
    // grid is the same and the data is mostly the same. This is based on the `key={...}` value
    // in row.map, so we need to tell our children nodes to re-render manually
    subject.next({
      reset: true
    })
  };

  randomSetAnyVisited = () => {
    const h = Math.floor(Math.random()*props.h);
    const w = Math.floor(Math.random()*props.w);
    const node = grid[h][w];
    subject.next({
      id: node.id,
      visited: true
    });
  };

  // Watch console.log to see how frequently parent renders
  console.log("rendering parent");

  return (
    <div>
      <table>
        <tbody>
          {grid.map((row, rowIndex) => (
            <tr key={`R${rowIndex}`}>
              {row.map((node, columnIndex) => (<GridNode {...node} observer={subject.asObservable()} key={node.id} />))}
            </tr>
          ))}
        </tbody>
      </table>
      <button onClick={randomSetAllVisited}>Random set all visited</button>
      <button onClick={randomSetAnyVisited}>Random set any visited</button>
    </div>
  );
};

const GridNode = (props) => {
  // We need to set to undefined to handle full grid reset from parent
  const [visited, setVisited] = React.useState(undefined);

  // Toggle visited based on props and if visited is undefined
  if (props.visited !== visited && visited === undefined) {
    setVisited(props.visited);
  }

  // bind all this with useEffect, so we can subscribe/unsubscribe, and not block rendering, `useEffect` is a good practice
  React.useEffect(() => {
    // notifications that come from parent node, `setVisited` will re-render this node
    const obs = props.observer.subscribe(next => {
      if (!!next.id && next.id === props.id) {
        setVisited(next.visited !== undefined ? next.visited : !visited);
      } else if (!!next.reset) {
        setVisited(undefined);
      }
    });
    return () => obs.unsubscribe();
  }, [visited]);

  handleMouseEnter = () => {
    setVisited(!visited);
  }

  handleMouseLeave = () => {
    setVisited(!visited);
  }

  classes = ["node"];
  if (visited) {
    classes.push("node-visited");
  }
  return (<td onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className={classes.join(" ")}/>);
}

在 codepen 示例中,我有一个 50x50 的网格,它没有卡顿、滞后或重新渲染子节点或更新它们的问题。 有两个帮助按钮可以为所有节点随机化 state,或随机化单个节点。 我已将其缩放超过 100x100,并且没有延迟或性能问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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