简体   繁体   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

I have been learning hooks in react for the past couple of days, and I tried creating a scenario where I need to render a big grid on screen, and update the background color of the nodes depending on the action I want to take.在过去的几天里,我一直在学习 react 中的钩子,我尝试创建一个场景,我需要在屏幕上渲染一个大网格,并根据我想要采取的操作更新节点的背景颜色。 There are two actions that will change the background color of a node, and these two actions must coexist.有两个动作会改变节点的背景颜色,这两个动作必须同时存在。

  • The cursor hovers a node while it is clicked. cursor 在单击节点时将其悬停。
  • There exists an algorithm inside the Grid component that will change backgrounds of some of the nodes. Grid组件内部存在一种算法,可以改变一些节点的背景。

The way I see it, there are multiple ways I can achieve this, but I am having some trouble with the way hooks were intended to be used.在我看来,有多种方法可以实现这一点,但我在使用钩子的方式上遇到了一些麻烦。 I will first walk you through my thought process on how this could be achieved from what I learned, and then show you the implementation that I tried.我将首先引导您了解如何从我学到的知识中实现这一点的思考过程,然后向您展示我尝试过的实现。 I tried to keep the important parts of the code so it can be understood clearly.我试图保留代码的重要部分,以便可以清楚地理解。 Please let me know if I missed somethings or misunderstood a concept completely.如果我错过了什么或完全误解了一个概念,请告诉我。

  1. The children can hold their own state and know how to update themselves.孩子们可以拿着自己的state,知道如何更新自己。 The parent can hold the reference to each children of the list, and call the necessary function from the reference of the child when it is needed in order to update the children.父级可以持有对列表中每个子级的引用,并在需要更新子级时从子级的引用中调用必要的function。

    • Works well for the first and the second action to be taken.适用于要采取的第一个和第二个动作。 This solution causes no performance issues since the children manage their own state, and if the parent updates the children state via reference, the only child to be re-rendered will be the one that gets called.此解决方案不会导致性能问题,因为孩子管理自己的 state,如果父通过引用更新孩子 state,则唯一要重新渲染的孩子将是被调用的孩子。
    • This solution is seen as an anti-pattern from what I read.从我阅读的内容来看,这个解决方案被视为一种反模式。

    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. The state of the children could be given as props from the parent, any update operation can be achieved inside the parent. 2、children的state可以作为来自parent的props,任何更新操作都可以在parent内部实现。 (Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter. If you move the mouse at a certain speed, nothing happens, and every visited node gets updated at once.) (子节点被正确更新,render 仅在必要的子节点中被调用,但 DOM 似乎结结巴巴。如果您以一定的速度移动鼠标,则什么也不会发生,并且每个访问的节点都会立即更新。)

  • Doesn't work for the first action.不适用于第一个动作。 Children gets updated correctly, render gets called in only the necessary children, but the DOM seems to stutter.子项得到正确更新,渲染只在必要的子项中被调用,但 DOM 似乎结结巴巴。 If you move the mouse at a certain speed, nothing happens and every visited node gets updated at once.如果您以一定的速度移动鼠标,则不会发生任何事情,并且每个访问的节点都会立即更新。

    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]);
    }


I have two questions that I want to ask on this topic.关于这个话题,我有两个问题想问。

  1. In the first implementation;在第一个实现中; the parent component doesn't re-render when a node changes its' state.当节点更改其 state 时,父组件不会重新渲染。 Is it wrong to just utilize this anti-pattern if it is beneficial in this kind of situations?如果在这种情况下有益,那么仅仅使用这种反模式是错误的吗?

  2. What may be the cause of the stutter that the second implementation suffers from?第二种实现遇到的口吃可能是什么原因? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。

As you say that using refs to control child data is an anti-pattern, However it doesn't mean that you cannot use it.正如您所说,使用 refs 来控制子数据是一种反模式,但这并不意味着您不能使用它。

What it means is that if there are better and more performant means, its better to use them as they lead to better readability of the code and also improve debugging.这意味着如果有更好和更高性能的方法,最好使用它们,因为它们可以提高代码的可读性并改善调试。

In your case using a ref definitely makes it easier to update state and also prevents a lot of re-rendering is a good way to implement the above solution在您的情况下,使用 ref 绝对可以更轻松地更新 state 并且还可以防止大量重新渲染是实现上述解决方案的好方法

What may be the cause of the stutter that the second implementation suffers from?第二种实现遇到的口吃可能是什么原因? I have spent a while reading the docs and trying out different things, but cannot find the reason of the stuttering that is happening.我花了一段时间阅读文档并尝试不同的东西,但找不到发生口吃的原因。

A lot of the problem in the second solution arise from the fact that you define functions which are recreated on each re-render and hence cause the entire grid to be re-rendered instead of just the cell.第二种解决方案中的许多问题都源于您定义了在每次重新渲染时重新创建的函数,因此导致整个网格被重新渲染,而不仅仅是单元格。 Make use of useCallback to memoize these function in Grid component利用 useCallback 在 Grid 组件中记住这些 function

Also you should use React.memo instead of useMemo for your usecase in GridNode.此外,您应该在useMemo中为您的用例使用React.memo而不是 useMemo。

Another thing to note is that you are mutating the state while updating, Instead you should update it in an immutable manner另一件需要注意的事情是,您在更新时正在改变 state,相反,您应该以不可变的方式更新它

Working code:工作代码:

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 While useCallback and other memoizations will help give to some performance benefits it will still not be able to overcome the performance impacts on state updates and re-render. PS 虽然useCallback和其他记忆将有助于提供一些性能优势,但它仍然无法克服对 state 更新和重新渲染的性能影响。 In such scenarios its better to make define state within the children and expose a ref for the parent在这种情况下,最好在子级中定义 state 并为父级公开一个参考

As stated, the solution is an anti-pattern because you're mixing the rendering and business logic on both levels.如前所述,该解决方案是一种反模式,因为您在两个级别上混合了呈现和业务逻辑。 You don't need to explicitly use React.forwardRef , in fact according to the docs you shouldn't, even when composing HOC (Higher order components).您不需要显式使用React.forwardRef ,实际上根据您不应该使用的文档,即使在编写 HOC(高阶组件)时也是如此。 You shouldn't need to directly access the element and do some sort of action on it - let React do its thing.你不需要直接访问元素并对其执行某种操作——让 React 做它的事情。 It's very good and efficient at it.这是非常好的和有效的。

Generally when you're calling a re-render method on a child node tree when there's n nodes, you don't want to cause a re-render from the top-level node, the parent in this case, because it will cause the entire node-tree to re-render into a new DOM element, rather than update existing elements.通常,当您在有 n 个节点的子节点树上调用重新渲染方法时,您不希望从顶级节点(在这种情况下为父节点)重新渲染,因为它会导致整个节点树重新渲染成一个新的 DOM 元素,而不是更新现有元素。

Your current solution has a combination of parent-triggered renders and child triggered renders.您当前的解决方案结合了父触发渲染和子触发渲染。 The React page has a good example with the tic-tac-toe application for how to render children without causing the parent to re-render. React 页面有一个很好的例子,带有井字游戏应用程序,说明如何在不导致父级重新渲染的情况下渲染子级。

The strategy that you should use is one where the parent node has an object structure, in this case n^2 nodes (eg 10x10 for arguments sake), is to pass the rendering functionality to the child nodes, and let the child nodes handle the rendering.您应该使用的策略是父节点具有 object 结构,在这种情况下为 n^2 个节点(例如,10x10 用于 arguments),是将渲染功能传递给子节点,并让子节点处理渲染。

When you're triggering a render from the parent node, you have a couple of options (assuming functional components) which really fall into the case of observable updates.当您从父节点触发渲染时,您有几个选项(假设是功能组件),它们确实属于可观察更新的情况。 You want to be able to push updates from the parent to the child, to modify the child node state, and let the child node update itself.您希望能够将更新从父节点推送到子节点,以修改子节点 state,并让子节点自行更新。

Here's an example with child nodes rendering, while the parent is communicating changes to the children.这是一个子节点渲染的示例,而父节点正在将更改传达给子节点。 You'll see that the performance scales well even up to massive grids, compared to the nested level renders your example has.与示例所具有的嵌套级别渲染相比,您会看到性能甚至可以扩展到大型网格。

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

This is achieved by using a combination of RxJS observable/subject, React.useState and React.useEffect .这是通过使用 RxJS observable/subject、 React.useStateReact.useEffect的组合来实现的。 We use useState in both the parent and child nodes to deal with rendering and prop updates, and useEffect to bind the observable.我们在父节点和子节点中都使用useState来处理渲染和道具更新,并useEffect来绑定 observable。 useState is persistent between renders, which means you don't need to rebuild the entire grid every time you update in the parent, but even if you do, React is intelligent enough to determine that you updated the props of a node, not replaced it. 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(" ")}/>);
}

In the codepen example, I have a 50x50 grid, that has no stutters, lag, or issues re-rendering the children nodes, or updating them.在 codepen 示例中,我有一个 50x50 的网格,它没有卡顿、滞后或重新渲染子节点或更新它们的问题。 There are two helper buttons to randomise the state for all nodes, or randomise a single node.有两个帮助按钮可以为所有节点随机化 state,或随机化单个节点。 I've scaled this over 100x100 and no lag or performance issues.我已将其缩放超过 100x100,并且没有延迟或性能问题。

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

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