Alternative React Portal implementation, giving you control over portal rendering.
Primarily written to support uninterrupted exit animations when combined with components such as TransitionGroup
and AnimatePresence
.
npm install react-teleportal
https://codesandbox.io/s/react-teleportal-x-react-transition-group-k31d8p
https://codesandbox.io/s/react-teleportal-x-framer-motion-766nu7
Features | React Teleportal | ReactDOM.createPortal |
---|---|---|
Custom Rendering | ✅ | ❌ |
Context | ✅* | ✅ |
Server Side Rendering (SSR) | ❌ | |
Multiple Portal Outlets | ❌‡ | ✅ |
React Tree Event Bubbling | ❌ | ✅ |
* Although <Portal />
s in React Teleportal don't receive context from their own call site, they do receive context from the <PortalOutlet />
call site which means context from root providers will be available.
† Unlike ReactDOM.createPortal
, React Teleportal doesn't depend on DOM APIs so the intention is to support SSR once a concurrent-safe solution has been found.
‡ React Teleportal doesn't currently support multiple portal outlets, but it would be trivial to add. For now it's been omitted because it would effectively become a "slot" library which, as a pattern, doesn't play nicely with streaming SSR.
import React, { useState } from 'react';
import { PortalProvider, PortalOutlet, Portal } from 'react-teleportal';
const App = () => {
const [show, setShow] = useState(true);
return (
<PortalProvider>
<button onClick={() => setShow(!show)}>Toggle</button>
{show ? (
<Portal>
<>I render in the PortalOutlet</>
</Portal>
) : null}
<PortalOutlet />
</PortalProvider>
);
};
Animations with react-transition-group
import React, { useState, useRef } from 'react';
import { PortalProvider, PortalOutlet, Portal } from 'react-teleportal';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
const App = () => {
const [show, setShow] = useState(true);
const nodeRef = useRef(null);
return (
<PortalProvider>
<button onClick={() => setShow(!show)}>Toggle</button>
{show ? (
<Portal>
<CSSTransition
// `key` ensures showing / hiding the portal will reverse an in-flight animation rather than create a new instance.
key="5f337061-5476-40a0-898e-e9f9827043b1"
nodeRef={nodeRef}
timeout={200}
classNames="my-node"
>
<div ref={nodeRef}>I render in the PortalOutlet</div>
</CSSTransition>
</Portal>
) : null}
<PortalOutlet>
{(children) => <TransitionGroup>{children}</TransitionGroup>}
</PortalOutlet>
</PortalProvider>
);
};
Animations with framer-motion
import React, { useState } from 'react';
import { PortalProvider, PortalOutlet, Portal } from 'react-teleportal';
import { AnimatePresence, motion } from 'framer-motion';
const App = () => {
const [show, setShow] = useState(true);
return (
<PortalProvider>
<button onClick={() => setShow(!show)}>Toggle</button>
{show ? (
<Portal>
<motion.div
// `key` ensures showing / hiding the portal will reverse an in-flight animation rather than create a new instance.
key="02fe2dd1-e9d8-46e4-898b-4c1966c9a68b"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
I render in the PortalOutlet
</motion.div>
</Portal>
) : null}
<PortalOutlet>
{(children) => <AnimatePresence>{children}</AnimatePresence>}
</PortalOutlet>
</PortalProvider>
);
};
React Teleportal won't blow up on the server, but <Portal />
s won't be rendered to HTML server side and instead will be rendered once on the client.
The intention is to eventually find a concurrent-safe SSR solution.
No not currently. React Teleportal intends to eventually support SSR & treating this as a "slot" library makes SSR less viable.
React Gateway is a good example of the "slot" pattern and how it can easily fail if misused.
import { GatewayProvider, GatewayDest, Gateway } from 'react-gateway';
const App = () => {
return (
<GatewayProvider>
<header>
<GatewayDest name="header-slot" />
</header>
<section>
<Gateway into="header-slot">
SSR will fail to render this as the "header-slot" has already rendered
(and if streaming, the html has potentially already been flushed to
the client).
</Gateway>
</section>
</GatewayProvider>
);
};
React Teleportal is therefore stricter and only allows a single <GatewayDest />
(or <PortalOutlet />
in React Teleportal terminology) which should be rendered at the bottom of the root component.
It's recommended to avoid z-index and treat your <PortalOutlet />
similar to the DOM's Top Layer whereby the most recently opened mounted <Portal />
is rendered last and therefore naturally stacked on top.
The collective <Portal />
children are ultimately rendered as children
of the <PortalOutlet />
which means React is rendering a variable length array of elements which requires a key
.
It's recommended to just statically include a uuid
or similar at the call site of each distinct <Portal />
child to ensure it remains unique as your app grows.
<Portal>
<div key="6db2c89c-dbb4-4c9e-96fa-8ad1d3dec463">Hello World</div>
</Portal>
NOTE: If you're not animating (i.e. if the
<PortalOutlet />
unmounts the child immediately), then you can omit the key as React Teleportal is able to assign a key on your behalf.