Wormhole在React TypeScript中不会渲染任何内容:createPortal + useContext

huangapple go评论66阅读模式
英文:

Wormhole in React typescript does not render anything: createPortal + useContext

问题

这是对应的游乐场。

目标: 基本上,我希望所有PortalEntrance的子元素都在PortalExit下渲染。

基本思路:

  • PortalProvider 提供一个ref
  • PortalExit 将这个ref 分配给它的一个子元素
  • PortalEntrance 使用第二个参数的ref 来调用 createPortal

为了完整起见,这是完整的游乐场代码:

import type { ReactNode } from "react";
import * as React from "react";
import { createPortal, render } from "react-dom";

type PortalContextType = {
  ref: React.MutableRefObject<HTMLDivElement>;
};

const PortalContext = React.createContext<PortalContextType>({
  ref: null
});

type PortalProviderProps = {
  children: ReactNode;
};

function PortalProvider({ children }: PortalProviderProps) {
  const ref = React.useRef<HTMLDivElement>(null);

  const value = React.useMemo(() => ({ ref }), [ref]);

  return (
    <PortalContext.Provider value={value}>{children}</PortalContext.Provider>
  );
}

function PortalExit() {
  const { ref } = React.useContext(PortalContext);

  return <div ref={ref}></div>;
}

type PortalEntranceProps = {
  children: ReactNode;
};

function PortalEntrance({ children }: PortalEntranceProps) {
  const { ref } = React.useContext(PortalContext);

  if (ref.current === null) {
    return <></>;
  }

  return createPortal(children, ref.current);
}

function App() {
  return (
    <PortalProvider>
      <div style={{ border: "1px solid green" }}>
        <PortalExit />
      </div>
      <div style={{ border: "1px solid red" }}>
        <PortalEntrance>my content</PortalEntrance>
      </div>
    </PortalProvider>
  );
}

const rootElement = document.getElementById("root");
render(<App />, rootElement);
英文:

This is the corresponding playground.

Goal: Basically, I want all children of PortalEntrance to be rendered under PortalExit.

Basic idea:

  • PortalProvider provides a ref
  • PortalExit assigns the ref to one its children
  • PortalEntrance uses the ref for the 2nd argument to createPortal

For completeness, here is the complete playground code:

import type { ReactNode } from &quot;react&quot;;
import * as React from &quot;react&quot;;
import { createPortal, render } from &quot;react-dom&quot;;

type PortalContextType = {
  ref: React.MutableRefObject&lt;HTMLDivElement&gt;;
};

const PortalContext = React.createContext&lt;PortalContextType&gt;({
  ref: null
});

type PortalProviderProps = {
  children: ReactNode;
};

function PortalProvider({ children }: PortalProviderProps) {
  const ref = React.useRef&lt;HTMLDivElement&gt;(null);

  const value = React.useMemo(() =&gt; ({ ref }), [ref]);

  return (
    &lt;PortalContext.Provider value={value}&gt;{children}&lt;/PortalContext.Provider&gt;
  );
}

function PortalExit() {
  const { ref } = React.useContext(PortalContext);

  return &lt;div ref={ref}&gt;&lt;/div&gt;;
}

type PortalEntranceProps = {
  children: ReactNode;
};

function PortalEntrance({ children }: PortalEntranceProps) {
  const { ref } = React.useContext(PortalContext);

  if (ref.current === null) {
    return &lt;&gt;&lt;/&gt;;
  }

  return createPortal(children, ref.current);
}

function App() {
  return (
    &lt;PortalProvider&gt;
      &lt;div style={{ border: &quot;1px solid green&quot; }}&gt;
        &lt;PortalExit /&gt;
      &lt;/div&gt;
      &lt;div style={{ border: &quot;1px solid red&quot; }}&gt;
        &lt;PortalEntrance&gt;my content&lt;/PortalEntrance&gt;
      &lt;/div&gt;
    &lt;/PortalProvider&gt;
  );
}

const rootElement = document.getElementById(&quot;root&quot;);
render(&lt;App /&gt;, rootElement);

答案1

得分: 3

假设它应该呈现“my content”消息,对吗?如果是这样 - 我有一个解决方案。

问题出在 ref 以及 React 处理渲染的方式上。React 会在某种状态更新时重新渲染组件。这为什么会成为问题呢?

在第一次渲染时,refnull,只有在初始渲染后才会获得它们的值(引用),但这不会触发重新渲染。

当你将这两者结合在一起时 - 你的组件 PortalEntrance 不会更新。你有一个逻辑,如果 ref.currentnull,则渲染一个空片段。否则 - 渲染一个 createPortal

function PortalEntrance({ children }: PortalEntranceProps) {
  const { ref } = React.useContext(PortalContext);

  if (ref.current === null) {
    return <></>;
  }

  return createPortal(children, ref.current);
}

所以在第一次渲染时,refnull,因此进入 if 语句。初始渲染后 - ref 不再是 null,但没有任何东西触发重新渲染。

即使它不会引起重新渲染,useEffect 钩子仍然可以捕获其值的变化。使用这个方法,你可以改变状态以强制重新渲染组件。

PortalEntrance 组件中,创建一个 useState,如 const [hasRef, setHasRef] = React.useState(false);。在 if 语句之前,创建一个监听 refuseEffect,如下所示:

React.useEffect(() => {
    setHasRef(!!ref.current);
  }, [ref]);

并且将你的 if 语句更改为检查 hasRef 而不是 ref.current 以渲染一个空片段。

你的组件应该如下所示:

function PortalEntrance({ children }: PortalEntranceProps) {
  const { ref } = React.useContext(PortalContext);

  const [hasRef, setHasRef] = React.useState(false);

  React.useEffect(() => {
    setHasRef(!!ref.current);
  }, [ref]);

  if (!hasRef) {
    return <></>;
  }

  return createPortal(children, ref.current);
}

这就是解决方法。工作示例链接:Working fork example

英文:

Assuming it should render "my content" message, right? If so - I have a solution.

The issue comes from ref and how rendering is handled by React. React re-renders the component if a state of some sort updates. How is this an issue?

Refs, on the first render, are null and only then obtain their value (reference) after the initial render but that does NOT trigger a re-render.

When you put these 2 together - your component PortalEntrance does not update. You have a logic that if ref.current is null then render an empty fragment. else - render a createPortal.

function PortalEntrance({ children }: PortalEntranceProps) {
const { ref } = React.useContext(PortalContext);
if (ref.current === null) {
return &lt;&gt;&lt;/&gt;;
}
return createPortal(children, ref.current);
}

So on the first render ref is null so it goes into the if. After the initial render - ref is no longer null BUT nothing triggers a re-render.

Even tho it does not cause a re-render, the useEffect hook can still catch its value changing. Using that, you can change the state yourself forcing to re-render the component.

In the PortalEntrance component, create a useState like const [hasRef, setHasRef] = React.useState(false);. Before the if, create a useEffect that is listening to the ref like so

React.useEffect(() =&gt; {
setHasRef(!!ref.current);
}, [ref]);

And change your if statement to check hasRef rather than ref.current for rendering an empty fragment.

Your component should look like so:

function PortalEntrance({ children }: PortalEntranceProps) {
const { ref } = React.useContext(PortalContext);
const [hasRef, setHasRef] = React.useState(false);
React.useEffect(() =&gt; {
setHasRef(!!ref.current);
}, [ref]);
if (!hasRef) {
return &lt;&gt;&lt;/&gt;;
}
return createPortal(children, ref.current);
}

This does the trick. Working fork example: https://codesandbox.io/s/summer-field-7zi1pm?file=/src/index.tsx:769-1093

huangapple
  • 本文由 发表于 2023年5月29日 05:10:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/76353612.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定