英文:
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 aref
PortalExit
assigns theref
to one its childrenPortalEntrance
uses theref
for the 2nd argument to createPortal
For completeness, here is the complete playground code:
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);
答案1
得分: 3
假设它应该呈现“my content”消息,对吗?如果是这样 - 我有一个解决方案。
问题出在 ref
以及 React 处理渲染的方式上。React 会在某种状态更新时重新渲染组件。这为什么会成为问题呢?
在第一次渲染时,ref
是 null
,只有在初始渲染后才会获得它们的值(引用),但这不会触发重新渲染。
当你将这两者结合在一起时 - 你的组件 PortalEntrance
不会更新。你有一个逻辑,如果 ref.current
是 null
,则渲染一个空片段。否则 - 渲染一个 createPortal
。
function PortalEntrance({ children }: PortalEntranceProps) {
const { ref } = React.useContext(PortalContext);
if (ref.current === null) {
return <></>;
}
return createPortal(children, ref.current);
}
所以在第一次渲染时,ref
是 null
,因此进入 if
语句。初始渲染后 - ref
不再是 null
,但没有任何东西触发重新渲染。
即使它不会引起重新渲染,useEffect
钩子仍然可以捕获其值的变化。使用这个方法,你可以改变状态以强制重新渲染组件。
在 PortalEntrance
组件中,创建一个 useState
,如 const [hasRef, setHasRef] = React.useState(false);
。在 if
语句之前,创建一个监听 ref
的 useEffect
,如下所示:
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 <></>;
}
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(() => {
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(() => {
setHasRef(!!ref.current);
}, [ref]);
if (!hasRef) {
return <></>;
}
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论