简体   繁体   English

如何仅使用 useRef 和 useEffect 重新渲染单个子组件

[英]How to only re-render individual child components with useRef and useEffect

tl;dr - how to force re-render only one specific child component by tracking ref ? tl; dr - 如何通过跟踪ref强制重新渲染仅一个特定的子组件?

I have a table of rows.我有一个行表。 I'd like to be able to hover on rows and show/hide a cell in the row, but only after a while.我希望能够在行上 hover 并在行中显示/隐藏一个单元格,但只能在一段时间之后。

You can only reveal hidden hover content after hovering over the entire table for some period of time - triggered by onMouseEnter and onMouseLeave .在将鼠标悬停在整个表格上一段时间后,您只能显示隐藏的 hover 内容 - 由onMouseEnteronMouseLeave触发。

Once hovering a particular <Row> , it should show the extra content if it's allowed to by the parent.一旦悬停特定的<Row> ,如果父级允许,它应该显示额外的内容。

The sequence for mouse over the table:鼠标悬停在表格上的顺序:

  1. Hover over row Hover 过排
  2. Row's isHovered is now true Row 的isHovered现在为true
  3. In 1000ms, allowHover changes to true在 1000 毫秒内, allowHover变为true
  4. Since allowHover and isHovered are both true , show extra row content因为allowHoverisHovered都是true ,所以显示额外的行内容

The sequence for mouse OUT the table:鼠标移出表格的顺序:

  1. Mouse moves outside of the parent container/table/row鼠标移出父容器/表/行
  2. Previously hovered row's isHovered is set to false之前悬停的行的isHovered设置为false
  3. Previously hovered row's hidden content is hidden先前悬停的行的隐藏内容被隐藏
  4. In 1000ms, allowHover changes to false在 1000 毫秒内, allowHover变为false

At this point, if re-entering the table, we'd have to wait for 1 second again before allowHover is true.此时,如果重新进入表格,我们必须再次等待 1 秒,然后allowHover为真。 Once both isHovered and allowHover are true, display hidden content.一旦isHoveredallowHover都为真,就显示隐藏的内容。 Once hover is allowed, there are no delays involved: rows hovered over should immediately reveal the hidden content.一旦 hover 被允许,就不会有任何延迟:悬停的行应该立即显示隐藏的内容。

I'm trying to employ useRef to avoid mutating state of the rows' parent and causing a re-render of all the child rows我正在尝试使用useRef来避免改变行的父行的 state 并导致重新渲染所有子行

At the row level, on hover, a row should be able to check if hover is allowed without the entire list being re-rendered with props.在行级别,在 hover 上,行应该能够检查是否允许 hover,而无需使用道具重新渲染整个列表。 I assumed useEffect could be set to track the value but it doesn't seem to trigger a re-render at the individual component level.我假设useEffect可以设置为跟踪该值,但它似乎不会触发单个组件级别的重新渲染。

In other words, expected behavior is for the currently hovered over row to detect the change in the parent and only re-render itself to reveal content .换句话说,预期的行为是当前悬停在行上以检测父级的更改,并且仅重新渲染自身以显示内容 Then, once hovering is allowed the behavior is straightforward.然后,一旦允许悬停,行为就很简单了。 Hover over row? Hover 过排? Reveal its content.揭示其内容。

Here's the snippets of code involved:以下是涉及的代码片段:

function Table() {
  const allowHover = useRef(false);

  const onMouseEnter = (e) => {
    setTimeout(() => {
      allowHover.current = true; // allow hovering
    }, 1000);
  };
  const onMouseLeave = (e) => {
    setTimeout(() => {
      allowHover.current = false; // dont allow hovering
    }, 1000);
  };

  return (
    <div className="App" style={{ border: '3px solid blue' }}>
      <h1>table</h1>
      {/* allow/disallow hovering when entering and exiting the table, with a delay */}
      <table onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
        <tbody>
          <AllRows allowHover={allowHover} />
        </tbody>
      </table>
    </div>
  );
}

function Rows(props) {
  return [1, 2, 3].map((id) => (
    <Row id={id} allowHover={props.allowHover} />
  ));
}

function Row(props) {
  let [isHovered, setIsHovered] = useState(false);

  useEffect(() => {
    // Why isn't this re-rendering this component?
  }, [props.allowHover]);

  const onMouseEnter = ({ target }) => {
    setIsHovered(true);
  };
  const onMouseLeave = ({ target }) => {
    setIsHovered(false);
  };

  console.log('RENDERING ROW');
  return (
    <tr key={props.id} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
      <td style={{ border: '1px solid red' }}>---------------- {props.id}</td>
      <td style={{ border: '1px solid green' }}>
        {props.allowHover.current && isHovered ? (
          <button>ACTIONS</button>
        ) : null}
      </td>
    </tr>
  );
}

Basics基本

React反应

One, if not THE , biggest of React's advantages is that it lets us manage an application with state without interacting with the DOM via manual updates and event listeners. React 最大的优势之一是它允许我们使用state管理应用程序,而无需通过手动更新和事件侦听器与 DOM 交互。 Whenever React "rerenders", it renders a virtual DOM which it then compares to the actual DOM, doing replacements where necessary.每当 React “重新渲染”时,它会渲染一个虚拟 DOM,然后将其与实际 DOM 进行比较,并在必要时进行替换。

Hooks挂钩

  • useState : returns a state and a setter, whenever the setter is executed with a new state, the component rerenders. useState :返回一个 state 和一个设置器,只要使用新的 state 执行设置器,组件就会重新渲染。
  • useRef : provides a possibility to keep a reference to a value or object between renders. useRef :提供了在渲染之间保持对值或 object 的引用的可能性。 It's like a wrapper around a variable which is stored at the ref's current property and which is referentially identical between renders as long as you don't change it.它就像一个变量的包装器,该变量存储在 ref 的current属性中,只要您不更改它,它在渲染之间的引用是相同的。 Setting a new value to a ref does NOT cause the component to rerender.为 ref 设置新值不会导致组件重新呈现。 You can attach actual DOM nodes to refs but you can really attach anything to a ref.您可以将实际的 DOM 节点附加到 refs,但您实际上可以将任何内容附加到 ref。
  • useEffect : code to run after a component has rendered. useEffect : 组件渲染后运行的代码。 The useEffect is executed after the DOM has been updated. useEffect在 DOM 更新后执行。
  • memo : adds possibility to manually control when a child rerenders after the parent has rerendered. memo :增加了在父级重新渲染后手动控制子级何时重新渲染的可能性。

Your code你的代码

I'm going to assume that this is some kind of toy example and that you want to understand how React works, if not, doing this by direct manipulation of DOM nodes is not the way to go.我将假设这是某种玩具示例,并且您想了解 React 是如何工作的,如果不是,那么通过直接操作 DOM 节点来做到这一点并不是通往 go 的方法。 As a matter of fact, you should ONLY use memo and performances improvements and direct manipulation of DOM nodes when it's not possible to accomplish what you want in another way, or if doing it the the regular way with React does not yield acceptable performance.事实上,你应该使用memo和性能改进以及直接操作 DOM 节点时无法以其他方式完成你想要的,或者如果使用 React 以常规方式执行它不会产生可接受的性能。 In your case, the performance would be good enough.在您的情况下,性能会足够好。

There are libraries that do most of the work outside of React.有一些库可以完成 React 之外的大部分工作。 One of those is react-spring which animates DOM elements.其中之一是为 DOM 元素设置动画的react-spring Doing these animations in React would slow them down and make them lag, therefore react-spring uses refs and updates DOM nodes directly, setting different CSS properties right on the element.在 React 中执行这些动画会减慢它们的速度并使其滞后,因此react-spring使用 refs 并直接更新 DOM 节点,在元素上设置不同的 CSS 属性。

Your questions你的问题

  • You wonder why the useEffect in Row is not triggered whenever you change the content of the ref.您想知道为什么当您更改 ref 的内容时不会触发Row中的useEffect Well, this is simply because useEffect runs after render, and there is no guarantee that the component will rerender just because you change the content of the allowHover ref.好吧,这仅仅是因为useEffect在渲染之后运行,并且不能保证仅仅因为您更改了allowHover ref 的内容,组件就会重新渲染。 The ref passed to AllRows and Row is the same property all the time (only its current property changes), therefore they will never rerender due to props being changed.传递给AllRowsRow的 ref 始终是相同的属性(仅更改其current属性),因此它们永远不会因为 props 被更改而重新渲染。 Since Row only rerenders, by itself, when isHovered is set, there is no guarantee that the useEffect will fire just because you change the content of allowHover ref.由于Row仅重新渲染,因此当设置isHovered时,不能保证仅因为您更改useEffect ref 的内容而allowHover WHEN Row rerenders, the effect will run IF the value of allowHover.current is different from last time.Row重新渲染时,如果 allowHover.current的值与allowHover.current不同,效果将运行。
  • Using memo will not help you here either since Table or AllRows don't rerender either.因为TableAllRows也不会重新渲染,所以在这里使用memo也无济于事。 memo allows us to skip rerendering children when parents rerender, but here, the parents don't rerender, so memo will do nothing. memo允许我们在父母重新渲染时跳过重新渲染孩子,但是在这里,父母不重新渲染,所以memo不会做任何事情。

All in all, neither useEffect or memo are some kind of magic functions that keep track of variables at all times and then do something when these change, instead, they are just functions that are executed at given times in the React lifecycle, evaluating the current context.总而言之,无论是useEffect还是memo都不是某种神奇的函数,它始终跟踪变量,然后在这些变量发生变化时做一些事情,相反,它们只是在 React 生命周期中的给定时间执行的函数,评估当前语境。

Your use case您的用例

Basically, whether a Row should be visible or not depends on two conditions:基本上, Row是否可见取决于两个条件:

  • Is allowHover.current set to true ? allowHover.current是否设置为true
  • Is isHovered set to true ? isHovered是否设置为true

Since these don't depend on each other, we should ideally like to be able to modify the conditional content from event listeners attached to both of the events which change the values of these properties.由于它们不相互依赖,理想情况下,我们应该希望能够修改附加到两个事件的事件侦听器的条件内容,这些事件会更改这些属性的值。

In a vanilla Javascript environment, we would perhaps store each element depending on this in an array and set its display or visibility from the event listeners which would check both of these conditions;在一个普通的 Javascript 环境中,我们可能会根据这个将每个元素存储在一个数组中,并从将检查这两个条件的事件侦听器设置其displayvisibility whichever event listener that fires last would be responsible for showing or hiding the component / row.最后触发的事件侦听器将负责显示或隐藏组件/行。

Doing the same in React, but bypassing React, should be quite straightforward as long as you can store this state in some ref.只要您可以将此 state 存储在一些参考文献中,在 React 中执行相同的操作,但绕过 React,应该非常简单。 Since both events occurring on Table level and Row level have to be able to modify the elements in question, access to these DOM elements must be available in both of these components.由于发生在Table级别和Row级别的事件都必须能够修改相关元素,因此对这些 DOM 元素的访问必须在这两个组件中都可用。 You can accomplish this by either merging the code of Table , Row and AllRows into one component or pass refs from the children back up to the parent component in some elaborate scheme.您可以通过将TableRowAllRows的代码合并到一个组件中来完成此操作,或者以某种精心设计的方案将来自子组件的 refs 传递回父组件。 The point here is, if you want to do it outside of React, ALL of this should be done outside of React.这里的重点是,如果你想在 React 之外进行,所有这些都应该在 React 之外完成。

Your current problem in the code is that you want to update one of the conditions ( allowHover ) outside of React but you want React to take care of the other condition ( isHovered ).您当前在代码中的问题是您想要更新 React 之外的一个条件( allowHover ),但您希望 React 处理另一个条件( isHovered )。 This creates an odd situation which is not advisable no matter if you would really want to do this outside of React (which I advise against in all cases except toy scenarios) or not.这会产生一种奇怪的情况,无论您是否真的想在 React 之外执行此操作(我建议在所有情况下都不要这样做,除了玩具场景),这都是不可取的。 React does not know when allowHover is set to true since this is done outside of React. React 不知道allowHover设置为true ,因为这是在 React 之外完成的。

Solutions:解决方案:

1. Use React 1. 使用反应

Simply use useState for the allowHover so that Table rerenders whenever allowHover changes.只需将useState用于allowHover ,以便TableallowHover更改时重新呈现。 This will update the prop in the children which will rerender too.这将更新子元素中的道具,这些道具也会重新渲染。 Also make sure to store the timeout in a ref so that you may clear it whenever you move the mouse in and out of the table.还要确保将超时存储在 ref 中,以便在将鼠标移入和移出表格时清除它。

With this solution, the Table and all its children will rerender whenever the mouse passes in and out of the table (after 1 s.) and then individual Row s will rerender whenever isHovered for that Row is changed.使用此解决方案,每当鼠标进出表格时(1 秒后), Table及其所有子项都会重新渲染,然后当该RowisHovered更改时,单独的Row将重新渲染。 The result is that Row s will rerender on both the conditions which control whether they should contain the conditional content or not.结果是Row将在控制它们是否应该包含条件内容的两个条件上重新呈现。

function Table() {

  const [allowHover, setAllowHover] = useState(false);
  const hoverTimeout = useRef(null)

  const onMouseEnter = (e) => {
    if (hoverTimeout.current !== null) clearTimeout(hoverTimeout.current);
    hoverTimeout.current = setTimeout(() => {
      console.log("Enabling hover")
      setAllowHover(true); // allow hovering
    }, 1000);
  }

  const onMouseLeave = (e) => {
    if (hoverTimeout.current !== null) clearTimeout(hoverTimeout.current);
    hoverTimeout.current = setTimeout(() => {
      console.log("Disabling hover")
      setAllowHover(false); // dont allow hovering
    }, 1000);
  }

  console.log("Rendering table")

  return (
    <div className="App" style={{ border: "3px solid blue" }}>
      <h1>table</h1>
      {/* allow/disallow hovering when entering and exiting the table, with a delay */}
      <table
        style={{ border: "3px solid red" }}
        onMouseEnter={onMouseEnter}
        onMouseLeave={onMouseLeave}
      >
        <tbody>
          <Rows allowHover={allowHover} />
        </tbody>
      </table>
    </div>
  );
}

function Rows(props) {
  console.log("Rendering rows")

  return [1, 2, 3].map((id) => (
    <Row id={id} key={id} allowHover={props.allowHover} />
  ));
}

function Row(props) {
  let [isHovered, setIsHovered] = useState(false);

  const onMouseEnter = ({ target }) =>
    setIsHovered(true)

  const onMouseLeave = ({ target }) => 
    setIsHovered(false)

  console.log("Rendering row")

  return (
    <tr key={props.id} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
      <td style={{ border: "1px solid red" }}>---------------- {props.id}</td>
      <td style={{ border: "1px solid green" }}>
        {props.allowHover && isHovered ? <button>ACTIONS</button> : null}
      </td>
    </tr>
  )
}

No funny business going on here, just plain React style.这里没有有趣的事情,只是简单的 React 风格。

Code sandbox: https://codesandbox.io/s/bypass-react-1a-i3dq8代码沙箱: https://codesandbox.io/s/bypass-react-1a-i3dq8

2. Bypass React 2.绕过反应

Even though not recommended, if you do this, you should do all the updates outside of React.即使不推荐,如果你这样做,你应该在 React 之外进行所有更新。 This means you can't depend on React to rerender child rows when you update the state of the Table outside of React.这意味着当您在 React 之外更新Table的 state 时,您不能依赖 React 重新呈现子行。 You could do this in many ways, but one way is for child Row s to pass their refs back up to the Table component which manually updates the Row s via refs.您可以通过多种方式执行此操作,但一种方法是让子Row将其 ref 传递回Table组件,该组件通过 ref 手动更新Row This is pretty much what React does under the hood actually.这几乎就是 React 实际上在幕后所做的事情。

Here, we add a lot of logic to the Table component which becomes more complicated but instead, the Row components lose some code:在这里,我们向Table组件添加了很多逻辑,这变得更加复杂,但是Row组件却丢失了一些代码:

function Table() {

  const allowHover = useRef(false)
  const timeout = useRef(null)
  const rows = useRef({})

  const onAddRow = (row, index) => {
    rows.current = {
      ...rows.current,
      [index]: rows.current[index]
        ? { ...rows.current[index], row }
        : { row, hovered: undefined }
    };
    onUpdate()
  };

  const onHoverRow = (hovered, index) => {
    rows.current = {
      ...rows.current,
      [index]: rows.current[index]
        ? { ...rows.current[index], hovered }
        : { hovered, row: undefined }
    };
    onUpdate()
  };

  const onUpdate = () => {
    Object.values(rows.current).forEach(({ row, hovered }) => {
      if (hovered && allowHover.current) {
        if (row) row.innerHTML = "<button>Accept</button>"
      } else {
        if (row) row.innerHTML = ""
      }
    });
  };

  const onMouseEnter = (e) => {
    if (timeout.current !== null) clearTimeout(timeout.current);
    timeout.current = setTimeout(() => {
      console.log("Enabling hover on table")
      allowHover.current = true // allow hovering
      onUpdate()
    }, 1000)
  };
  const onMouseLeave = (e) => {
    if (timeout.current !== null) clearTimeout(timeout.current);
    timeout.current = setTimeout(() => {
      console.log("Disabling hover on table")
      allowHover.current = false // dont allow hovering
      onUpdate()
    }, 1000)
  };

  console.log("Rendering table")

  return (
    <div className="App" style={{ border: "3px solid blue" }}>
      <h1>table</h1>
      {/* allow/disallow hovering when entering and exiting the table, with a delay */}
      <table onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
        <tbody>
          <Rows onAddRow={onAddRow} onHoverRow={onHoverRow} />
        </tbody>
      </table>
    </div>
  );
}

function Rows(props) {

  console.log("Rendering rows")

  return [1, 2, 3].map((id, index) => (
    <Row
      key={id}
      index={index}
      id={id}
      onAddRow={props.onAddRow}
      onHoverRow={props.onHoverRow}
    />
  ))
}

function Row(props) {

  const onMouseEnter = ({ target }) =>
    props.onHoverRow(true, props.index)

  const onMouseLeave = ({ target }) =>
    props.onHoverRow(false, props.index)

  console.log("Rendering row")

  return (
    <tr onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} key={props.id}>
      <td style={{ border: "1px solid red" }}>---------------- {props.id}</td>
      <td
        ref={(ref) => props.onAddRow(ref, props.index)}
        style={{ border: "1px solid green" }}
      ></td>
    </tr>
  )
}

Code sandbox: https://codesandbox.io/s/bypass-react-1b-rsqtq代码沙箱: https://codesandbox.io/s/bypass-react-1b-rsqtq

You can see for yourself in the console that each component only renders once.您可以在控制台中亲自看到每个组件只呈现一次。

Conclusion结论

Always first implement things inside React the usual way, then use memo , useCallback , useMemo and refs to improve performance where absolutely necessary.总是首先以通常的方式在 React 中实现一些东西,然后在绝对必要的地方使用memouseCallbackuseMemo和 refs 来提高性能。 Remember that more complicated code also comes at a cost so just because you're saving some rerenderings with React doesn't mean you have arrived at a better solution.请记住,更复杂的代码也是有代价的,所以仅仅因为您使用 React 保存了一些重新渲染并不意味着您已经找到了更好的解决方案。

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

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