A portal renders React children into a DOM container outside their parent's physical DOM subtree while keeping them in the same React tree. Implement a Portal component that either borrows a caller-provided container or creates one host under document.body. Follow React's createPortal contract, including context continuity and React-tree event bubbling.
type PortalProps = {
children: React.ReactNode;
container?: Element | DocumentFragment | null;
};
function Portal({ children, container = null }: PortalProps): React.ReactPortal | null;
<Portal>
<div>Notifications</div>
</Portal>
// After the component mounts, one marked host is appended to document.body.
// The div renders inside that host. Unmounting removes the owned host.
const sidebar = document.querySelector('#sidebar');
<Portal container={sidebar}>
<AccountMenu />
</Portal>
// AccountMenu renders inside #sidebar. The Portal never removes #sidebar.
null on the first render. If document is unavailable, keep rendering null.div, set data-uiready-portal="", append it to document.body, and reuse it across ordinary rerenders.container.container changes.You will separate a portal's logical React parentage from its physical DOM target, then make target ownership explicit.
A tooltip may belong to a button in React but need to escape a clipping container in the DOM. React's createPortal performs that move without creating a second React root. Your component must also decide who owns the destination: it may borrow a supplied node, or create and later remove its own body-mounted host.
Picture two maps of the same child. The React tree keeps the child under Portal, so context and synthetic events follow that relationship. The DOM tree places the child's nodes inside a separate target.
const { createPortal } = require('react-dom');
function Portal({ children, container }) {
const target = container || document.body.appendChild(document.createElement('div'));
return createPortal(children, target);
}
This creates a new host during every render, so parent updates leave empty nodes behind. It also touches document while rendering, which fails in non-browser rendering, and it never removes the node it appended. The expression hides the essential ownership distinction between a borrowed target and a host the component created.
const React = require('react');
const { createPortal } = require('react-dom');
function Portal({ children, container = null }) {
const [target, setTarget] = React.useState(null);
React.useEffect(() => {
// Effects do not run during server rendering, so DOM access stays here.
if (typeof document === 'undefined') {
setTarget(null);
return undefined;
}
const node = container || document.createElement('div');
const ownsNode = container === null;
if (ownsNode) {
node.setAttribute('data-uiready-portal', '');
document.body.appendChild(node);
}
setTarget(node);
return () => {
// Borrowed containers outlive this component; owned hosts do not.
if (ownsNode) node.remove();
};
}, [container]);
if (target === null) return null;
return createPortal(children, target);
}
module.exports = { Portal };
The initial null target makes the render phase independent of the browser. One effect acquires a destination and returns the matching release step. When container changes, React runs the previous cleanup before setting up the next effect, so an old owned host disappears while external containers remain untouched.
Suppose <Portal><Toast /></Portal> mounts without a container.
target === null, so it returns null and performs no DOM work.div, adds the empty data-uiready-portal attribute, appends it to document.body, and stores it as the target.createPortal now places Toast inside that exact host.container prop is still null, so the effect does not restart and the same host receives the updated child.container={sidebar}. Cleanup removes the owned host. The next effect stores sidebar without appending it, and React recreates the portal content there.sidebar, but the effect does not remove sidebar because the component never owned it.document. Fix: acquire the host in an effect.container.remove() on cleanup can delete DOM owned by a page shell or third-party widget. Fix: track ownership and remove only self-created hosts.children replaces the host whenever content changes. Fix: depend only on container.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A portal renders React children into a DOM container outside their parent's physical DOM subtree while keeping them in the same React tree. Implement a Portal component that either borrows a caller-provided container or creates one host under document.body. Follow React's createPortal contract, including context continuity and React-tree event bubbling.
type PortalProps = {
children: React.ReactNode;
container?: Element | DocumentFragment | null;
};
function Portal({ children, container = null }: PortalProps): React.ReactPortal | null;
<Portal>
<div>Notifications</div>
</Portal>
// After the component mounts, one marked host is appended to document.body.
// The div renders inside that host. Unmounting removes the owned host.
const sidebar = document.querySelector('#sidebar');
<Portal container={sidebar}>
<AccountMenu />
</Portal>
// AccountMenu renders inside #sidebar. The Portal never removes #sidebar.
null on the first render. If document is unavailable, keep rendering null.div, set data-uiready-portal="", append it to document.body, and reuse it across ordinary rerenders.container.container changes.You will separate a portal's logical React parentage from its physical DOM target, then make target ownership explicit.
A tooltip may belong to a button in React but need to escape a clipping container in the DOM. React's createPortal performs that move without creating a second React root. Your component must also decide who owns the destination: it may borrow a supplied node, or create and later remove its own body-mounted host.
Picture two maps of the same child. The React tree keeps the child under Portal, so context and synthetic events follow that relationship. The DOM tree places the child's nodes inside a separate target.
const { createPortal } = require('react-dom');
function Portal({ children, container }) {
const target = container || document.body.appendChild(document.createElement('div'));
return createPortal(children, target);
}
This creates a new host during every render, so parent updates leave empty nodes behind. It also touches document while rendering, which fails in non-browser rendering, and it never removes the node it appended. The expression hides the essential ownership distinction between a borrowed target and a host the component created.
const React = require('react');
const { createPortal } = require('react-dom');
function Portal({ children, container = null }) {
const [target, setTarget] = React.useState(null);
React.useEffect(() => {
// Effects do not run during server rendering, so DOM access stays here.
if (typeof document === 'undefined') {
setTarget(null);
return undefined;
}
const node = container || document.createElement('div');
const ownsNode = container === null;
if (ownsNode) {
node.setAttribute('data-uiready-portal', '');
document.body.appendChild(node);
}
setTarget(node);
return () => {
// Borrowed containers outlive this component; owned hosts do not.
if (ownsNode) node.remove();
};
}, [container]);
if (target === null) return null;
return createPortal(children, target);
}
module.exports = { Portal };
The initial null target makes the render phase independent of the browser. One effect acquires a destination and returns the matching release step. When container changes, React runs the previous cleanup before setting up the next effect, so an old owned host disappears while external containers remain untouched.
Suppose <Portal><Toast /></Portal> mounts without a container.
target === null, so it returns null and performs no DOM work.div, adds the empty data-uiready-portal attribute, appends it to document.body, and stores it as the target.createPortal now places Toast inside that exact host.container prop is still null, so the effect does not restart and the same host receives the updated child.container={sidebar}. Cleanup removes the owned host. The next effect stores sidebar without appending it, and React recreates the portal content there.sidebar, but the effect does not remove sidebar because the component never owned it.document. Fix: acquire the host in an effect.container.remove() on cleanup can delete DOM owned by a page shell or third-party widget. Fix: track ownership and remove only self-created hosts.children replaces the host whenever content changes. Fix: depend only on container.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.