Skip to content

Commit 91c1e57

Browse files
committed
feat: Portal Component 기능 개선
1 parent 3a047a5 commit 91c1e57

File tree

3 files changed

+94
-15
lines changed

3 files changed

+94
-15
lines changed

src/components/Portal/index.tsx

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,92 @@
1-
import React, { useEffect, useState } from 'react';
2-
import ReactDOM from 'react-dom';
1+
import React from 'react';
2+
import {
3+
createContext,
4+
useCallback,
5+
useContext,
6+
useLayoutEffect,
7+
useMemo,
8+
} from 'react';
9+
import { createPortal } from 'react-dom';
10+
import { useIsMounted } from '../../hooks/useIsMounted';
311

412
interface PortalProps {
5-
id?: string;
613
children: React.ReactNode;
14+
className?: string;
15+
containerRef?: React.RefObject<HTMLElement | null>;
716
}
817

9-
const Portal = ({ id, children }: PortalProps) => {
10-
const [portalElement, setPortalElement] = useState<Element | null>(null);
18+
const portalContext = createContext<{ parentPortal: HTMLElement | null }>({
19+
parentPortal: null,
20+
});
1121

12-
useEffect(() => {
13-
setPortalElement(document.querySelector(`#${id}`));
14-
}, [id]);
22+
const PORTAL_DEFAULT_CLASS = 'thumbnail-generator';
1523

16-
if (!portalElement) return <>{children}</>;
17-
return ReactDOM.createPortal(children, portalElement);
18-
};
24+
function RenderPortal({ children, className, containerRef }: PortalProps) {
25+
const { parentPortal } = useContext(portalContext);
1926

20-
export default Portal;
27+
const getPortalNode = useCallback(
28+
(mountNode: HTMLElement) => {
29+
const portalNode = mountNode.ownerDocument.createElement('div');
30+
portalNode.classList.add(className || PORTAL_DEFAULT_CLASS);
31+
32+
return portalNode;
33+
},
34+
[className]
35+
);
36+
37+
/**
38+
* This is the mount node to render portal nodes.
39+
* The mountNode has the value "containerRef.current" if it has a "containerRef", or the parent portal node if it is a nested portal.
40+
* By default, it has "document.body".
41+
*/
42+
const mountNode = useMemo(() => {
43+
if (parentPortal) {
44+
return parentPortal;
45+
}
46+
47+
if (containerRef?.current) {
48+
return containerRef.current;
49+
}
50+
51+
return document.body;
52+
}, [parentPortal, containerRef]);
53+
54+
const portalNode = useMemo(
55+
() => getPortalNode(mountNode),
56+
[getPortalNode, mountNode]
57+
);
58+
59+
useLayoutEffect(() => {
60+
mountNode.appendChild(portalNode);
61+
62+
/**
63+
* "portalNode" is removed from "mountNode" on unmount.
64+
*/
65+
return () => {
66+
if (mountNode.contains(portalNode)) {
67+
mountNode.removeChild(portalNode);
68+
}
69+
};
70+
}, [portalNode, mountNode]);
71+
72+
return createPortal(
73+
<portalContext.Provider value={{ parentPortal: portalNode }}>
74+
{children}
75+
</portalContext.Provider>,
76+
portalNode
77+
);
78+
}
79+
80+
/** @tossdocs-ignore */
81+
export default function Portal({ children, ...restProps }: PortalProps) {
82+
const isMounted = useIsMounted();
83+
84+
/**
85+
* With this code, it is possible to solve the "window is not defined" and "Hydration Error" that can occur in SSR.
86+
*/
87+
if (!isMounted) {
88+
return <></>;
89+
}
90+
91+
return <RenderPortal {...restProps}>{children}</RenderPortal>;
92+
}

src/hooks/useIsMounted.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useEffect, useState } from 'react';
2+
3+
export function useIsMounted() {
4+
const [mounted, setMounted] = useState(false);
5+
useEffect(() => {
6+
setMounted(true);
7+
}, []);
8+
return mounted;
9+
}

src/lib/ThumbnailGenerator.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import Portal from '@components/Portal';
77
import Icon from '@components/Icon';
88

99
interface ThumbnailGeneratorProps {
10-
id?: string;
1110
isDefaultOpen?: boolean;
1211
buttonIcon?: React.ReactNode;
1312
iconSize?: 'small' | 'medium' | 'large';
@@ -19,7 +18,6 @@ interface ThumbnailGeneratorProps {
1918
}
2019

2120
const ThumbnailGenerator = ({
22-
id,
2321
buttonIcon,
2422
isDefaultOpen = false,
2523
iconSize = 'medium',
@@ -37,7 +35,7 @@ const ThumbnailGenerator = ({
3735

3836
return (
3937
<>
40-
<Portal id={id}>
38+
<Portal>
4139
{isOpen ? (
4240
<TG
4341
isFullWidth={isFullWidth}

0 commit comments

Comments
 (0)