react 重复提交_可重复使用的React门户

react 重复提交

The React docs say this about Portals:

React文档说了有关Portal的内容

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

门户网站提供了一种一流的方法来将子级呈现到父组件的DOM层次结构之外的DOM节点中。

This means that they are the tool to use when we need to render parts of our UI in a different layer. What makes them awesome it’s how portals behave:

这意味着它们是当我们需要在不同图层中呈现部分UI时使用的工具。 是什么让它们如此出色,这是门户网站的行为方式:

Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Features like context work exactly the same regardless of whether the child is a portal, as the portal still exists in the React tree regardless of position in the DOM tree.

即使门户可以在DOM树中的任何位置,它的行为在其他方面也都像普通的React子代。 不管上下文是否是子门户,诸如上下文之类的功能都完全相同,因为无论DOM树中的位置如何,门户仍存在于React树中。

Neat.

整齐。

A far as the API goes, using a portal is pretty straightforward. Providing that you have a div#portal in your HTML template, you just use ReactDom.createPortal:

就API而言,使用门户非常简单。 假设您在HTML模板中有一个div#portal ,则只需使用ReactDom.createPortal

import React from "react";
import { createPortal } from "react-dom";


const MyComponent = () => (
  <>
    <p>This renders in the app tree</p>
    {createPortal(<p>This renders in a portal</p>, document.getElementById("portal"))}
  </>
);

Quite simple. However, we are talking React here and, when using React, we tend to build reusable components (to encapsulate low level details, like finding the root DOM node).

非常简单。 但是,我们在这里谈论的是React,使用React时,我们倾向于构建可重用的组件(以封装底层细节,例如查找根DOM节点)。

<Portal rootId="portal">
  <p>This renders in a portal</p>
</Portal>

我们当前的门户组件(Our current Portal component)

Our reusable Portal component allows us to render stuff inside a portal. It creates the portal container in the DOM on demand and remove it when is no longer needed. Also we can share the container between different portals.

我们可重用的Portal组件使我们可以在Portal内渲染内容。 它按需在DOM中创建门户容器,并在不再需要时将其删除。 我们也可以在不同门户之间共享容器。

const Portal = ({ rootId, children }) => {
  const target = useRef(null);


  useEffect(() => {
    let container = document.getElementById(rootId);
    if (!container) {
      container = document.createElement("div");
      container.setAttribute("id", rootId);
      document.body.appendChild(container);
    }


    container.appendChild(target.current);


    return () => {
      target.current.remove();
      if (container.childNodes.length === 0) {
        container.remove();
      }
    };
  }, [rootId]);


  if (!target.current) {
    target.current = document.createElement("div");
  }


  return createPortal(children, target.current);
};

There are other different approaches to this out on the wild with different features: some don’t allow sharing, others use state instead of refs and render the portal in two steps, etc.

还有其他具有不同功能的不同方法,这些方法具有不同的功能:某些方法不允许共享,另一些方法则使用状态代替ref并分两步渲染门户,等等。

Try now this example:

现在尝试以下示例:

const Example = () => {
  const [visible, setVisible] = useState(false);


  return (
    <>
      <button onClick={() => setVisible(!visible)}>Show</button>
      {visible && (
        <>
          <Portal rootId="shared">
            <p>One</p>
          </Portal>
          <Portal rootId="shared">
            <p>Two</p>
          </Portal>
          <Portal rootId={new Date().getTime()}>
            <p>Three</p>
          </Portal>
        </>
      )}
    </>
  );
};

You’ll notice that, while we share containers between portals, there is always a wrapper div surrounding the portal children 😔.

您会注意到,尽管我们在门户网站之间共享容器,但始终在门户网站子项surrounding周围有一个包装div。

Image for post

There’s a reason for this. Bear with me.

这是有原因的。 忍受我。

试图避免包装 (Trying to avoid the wrapper)

Let’s try not to use the wrapper. Just simplify the component and do what naturally comes to mind:

让我们尝试不使用包装器。 只需简化组件并做自然想到的事情即可:

const Portal = ({ rootId, children }) => {
  const target = useRef(document.getElementById(rootId));


  useEffect(() => {
    return () => {
      if (target.current.childNodes.length === 0) {
        target.current.remove();
        target.current = null;
      }
    };
  }, [rootId]);


  if (!target.current) {
    target.current = document.createElement("div");
    target.current.setAttribute("id", rootId);
    document.body.appendChild(target.current);
  }


  return createPortal(children, target.current);
};

This should work. Let’s see what happens if we show and hide our portals several times…. the cleanup fails! 😅

这应该工作。 让我们看看如果多次显示和隐藏门户会发生什么……。 清理失败! 😅

Image for post

我勒个去! (What the hell!)

You can take a long hard look at the code and have a difficult time spotting the problem. After a little debugging you’ll get to see that at the time we execute our useEffect cleanup the container still contains the old child nodes 🤨.

您可以仔细看一下代码,很难发现问题所在。 经过一些调试之后,您会发现在执行useEffect清理时,容器仍包含旧的子节点🤨。

But… shouldn’t the DOM be updated according to the docs:

但是……不应该根据文档更新DOM:

The function passed to useEffect will run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.

传递给useEffect的函数将在将渲染提交到屏幕后运行。 将效果视为从React的纯功能性世界到命令性世界的逃生门。

The function passed to useEffect fires after layout and paint, during a deferred event.

传递给useEffect的函数会延迟事件期间布局和绘制触发。

It seems that the new DOM has already been created when the cleanup effect is run, but it it’s not committed yet. Checking the DOM as we did yielded obsolete results 😔.

运行清除效果时,似乎已经创建了新的DOM,但尚未提交。 像我们一样检查DOM会得出过时的结果😔。

And, that’s why the wrapper version works. By using a detached wrapper, the container/children relation is not subject to the React lifecycle. It doesn’t matter that the wrapper still contains old DOM nodes. We can detach it from the container and check if there are any attached wrappers left before React cleans up the node contents.

并且,这就是包装器版本起作用的原因。 通过使用独立的包装器,容器/子级关系不受React生命周期的约束。 包装器仍然包含旧的DOM节点都没关系。 我们可以将其从容器中分离出来,并在React清理节点内容之前检查是否有任何附加的包装器。

But, as I said before, this shouldn’t happen. In fact, using the experimental version of React it works as expected. This smells like a problem with how React fibers work. Maybe it’s something portal related only 🤔. To get to the bottom it’s mandatory to dig deeper into the React codebase (or wait for someone to enlighten us in the comments 😇).

但是,正如我之前所说,这不应该发生。 实际上,使用React的实验版本可以正常工作。 这闻起来像是React纤维如何工作的问题。 也许这只是门户相关的内容。 为了深入了解,必须深入研究React代码库(或等待有人在注释en中启迪我们)。

可能的解决方案 (A possible solution)

If the problem is that the DOM is not committed when we do our cleanup, maybe we can wait a little longer and allow React to really flush the DOM. Let’s schedule our cleanup for later using requestAnimationFrame.

如果问题是清理时未提交DOM,也许我们可以再等一会,然后让React真正刷新DOM。 让我们安排清理工作,以便稍后使用requestAnimationFrame进行

const Portal = ({ rootId, children }) => {
  const target = useRef(document.getElementById(rootId));


  useEffect(() => {
    return () => {
      window.requestAnimationFrame(() => {
        if (target.current.childNodes.length === 0) {
          target.current.remove();
          target.current = null;
        }
      });
    };
  }, [rootId]);


  if (!target.current) {
    target.current = document.createElement("div");
    target.current.setAttribute("id", rootId);
    document.body.appendChild(target.current);
  }


  return createPortal(children, target.current);
};

No wrapper and the cleanup works as expected.

没有包装,清理工作按预期进行。

We have mixed feeling about this solution. We are not sure we would use it in production code. We are cheating and we are making too many assumptions about how React works internally. Would this work with the new concurrent mode? It does now. Would this work in future React versions? Who knows 🤷🏽‍♀️.

我们对此解决方案有不同的感觉。 我们不确定是否会在生产代码中使用它。 我们在作弊,并且对React在内部如何工作做出了太多假设。 可以在新的并发模式下使用吗? 现在可以了。 在将来的React版本中可以使用吗? 谁知道🤷🏽‍♀️。

加起来 (Summing up)

What seemed an easy task: creating a reusable Portal component, was harder than we thought.

看起来容易完成的任务:创建可重用的Portal组件比我们想象的要难。

Toying with this component reminded us that it’s fundamental to know how React works under the hood. Make no assumptions and try not to bend React too much to avoid problems.

玩弄这个组件会提醒我们,了解React如何在后台工作是至关重要的。 不要做任何假设,并尽量不要将React弯曲得太多,以免出现问题。

翻译自: https://medium.com/trabe/reusable-react-portals-17dead20232b

react 重复提交

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值