httptoolkit / react-reverse-portal Goto Github PK
View Code? Open in Web Editor NEWReact reparenting :atom_symbol: Build an element once, move it anywhere
Home Page: https://httptoolkit.com
License: Apache License 2.0
React reparenting :atom_symbol: Build an element once, move it anywhere
Home Page: https://httptoolkit.com
License: Apache License 2.0
Hi! Really cool library, thanks for building it.
I've found though that if I use an iframe to render an embedded video, changing the parent will reload the embed from source each time. The actual source URL of the iframe doesn't matter. Do you have any idea why this might be the case?
Should the useMemo
example have an empty array as the second parameter?
const portalNode = React.useMemo(() => portals.createPortalNode(), []);
Thanks!
See this example: https://codesandbox.io/s/nifty-fire-ihho4?file=/src/index.tsx
When switching the OutPortal, the child component of the InPortal (i.e. the "expensive" component) appears to get re-rendered, although the component that is rendering the InPortal doesn't get re-rendered.
Is this a bug or is there a problem with that example?
I was wondering if it would be possible to use reversed portals in a React Native project.
Can we use this library in production level code or there are some limitations which should be highlighted ?
I have following queries around it though :-
@pimterry ^^
I have a component
<ComponentWithVideo />
that contains a list of tracks that are being rendered in a single <video/>
element.
That I rendered with <InPortal />
and displayed to <OutPortal />
This error occurs when I add another track and <ComponentWithVideo(s) />
re-renders.
I don't know if this is an issue I should raise with you, or the react-new-window maintainers, but I'll start here.
I'm using react-reverse-portal along with react-new-window to make a pop-out window... initially the portal is rendered inside a div with window-like chrome to allow dragging and resizing, but the user can click a "pop out" button to have the portal in an actual window outside their browser.
This used to work, but after upgrading react-new-window from version 0.1.2 to 0.1.3 I started to get the error "html" portalNodes must be used with html elements, but OutPortal is within <DIV>
It's correct, the parent of the OutPortal inside the new window is a DIV, but that doesn't seem to be a problem - nearly every example of react-reverse-portal puts the OutPortal in something other than the root of the page, and they don't complain. So, while the fix for me is to downgrade react-new-window, I'm uncertain why react-reverse-portal is complaining.
Here's a codesandbox demonstrating the issue: https://codesandbox.io/s/happy-benji-20o5q?file=/src/App.js
It won't work due to cross-origin issues inside the codesandbox editor, but you if open the page in a new window (or navigate directly to https://20o5q.csb.app/ ) you can press the "pop out" button and observe the issue. If you downgrade react-new-window to 0.1.2, the codesandbox will start to work as expected.
Thanks!
Hi, thanks for your lib it's been super helpful for us.
We've been using it to render a 3D app in different pages of our react application.
However for this to work well, we need the 3D to take 100% of the space of its container. This is broken in reverse-portal because of the intermediary div you create (document.createElement('div')), which does not have any style, classname, or way to inject those.
We've been using nth-child(1) in React as a work around, but moving to preact, it seems it's not always reliable (I believe sometimes the placeholder div remains in the DOM). Could you add a way for us to inject either a style or class name to that div?
Has anyone tried to run this on Nextjs? Getting an error:
"Cannot read property 'getInitialPortalProps' of null"
We use an Intersection Observer to determine whether a video is in view and to render it. The problem arises when the video leaves all OutPortals: it maintains the props that it received before going back 'into hiding', so inView
prop never gets set to false and
Is there a way to force the detached component to revert back to default props whenever it is hidden? Or some sort of workaround? I'm just looking for any way for the component to have state that triggers when it is detached from the DOM.
Hello, thank you for the library, it saves me now.
I'm using it for the audio widget component which should be started at the audio page and then be available for the user while they're browsing through the website.
I had one error which took me some time to figure out how to resolve. I did a deep dive into the component's functionality and wanted to share the insight, so next time someone else experiencing it could save their time.
The issue I've had is that I needed to re-create the node prop based on the id I have, but OutPortal's componentDidMount
was failing because of the error being thrown at the .mount()
method:
Cannot read property 'replaceChild' of null
https://github.com/httptoolkit/react-reverse-portal/blob/master/src/index.tsx#L47-L50
So here is how I was using the react-reverse-portal:
const MyComponent = () => {
const { setPortalNode } = usePortalNodeReference();
const { Id, asWidget } = useDataState();
const portalNode = useMemo(() => createPortalNode(), [Id]);
setPortalNode(portalNode);
return (
<>
<InPortal node={portalNode}><AudioComponent /></InPortal>
{asWidget && <OutPortal name="widget" node={portalNode} Id={Id}></OutPortal>}
</>
);
}
const AnotherComponent = () => {
const { Id, asWidget } = useDataState();
const { portalNode } = usePortalNodeReference();
return !asWidget && <OutPortal name="page" node={portalNode}/>
}
Every time, portalNode
or Id
prop was updating, OutPortal would call the .mount()
and it would throw an error.
I debugged it and this is because OutPortal's placeholder reference is missing the parentNode once .replaceChild
was called on it, and then after the update, it tries to use the same placeholder's parentNode again in the mount assuming that it is present:
https://github.com/httptoolkit/react-reverse-portal/blob/master/src/index.tsx#L148
I cannot tell why exactly the placeholderNode
doesn't get re-rendered on prop change and get a parent node again, but I feel is because of .replaceChild()
method call at .mount()
is breaking react's virtual dom representation.
At the end of the day I came to the solution where I do not re-create portalNode
, but use key
prop to force re-mount OutPortal
and AudioComponent
when the Id
prop changes:
const MyComponent = () => {
const { setPortalNode } = usePortalNodeReference();
const { Id, asWidget } = useDataState();
- const portalNode = useMemo(() => createPortalNode(), [Id]);
+ const portalNode = useMemo(() => createPortalNode(), []);
setPortalNode(portalNode);
return (
<>
- <InPortal node={portalNode}><AudioComponent /></InPortal>
+ <InPortal node={portalNode}><AudioComponent key={Id} /></InPortal>
- {asWidget && <OutPortal name="widget" node={portalNode} Id={Id}></OutPortal>}
+ {asWidget && <OutPortal key={Id} name="widget" node={portalNode} Id={Id}></OutPortal>}
</>
);
}
Having all of this information, I think it is sensible to have a check for placeholders parent node before using it, also react-reverse-portal could give component's user a warning with an explanation of what is happening.
It looks like this repo is still using travis-ci.org for CI, which is deprecated and slated for shutdown at some point.
Migrating to travis-ci.com is an option, but personally I've found Github Actions to be a nicer experience. If it sounds worthwhile and desirable, I'd be happy to submit a PR to migrate from Travis to Github Actions -- likely a diff similar to https://github.com/spautz/limited-cache/pull/38/files
Or if some other CI system seems better, it may be worth looking at before Travis kills off their .org service
Portals are stored in the fiber context, as far as I can tell, and InPortal creates a portal, which is then saved to fiber (through functional component return statement). Such registration (context) is required to activate the portal.
containers are replaced automatically by react-reverse-portal, but how to destroy the portal itself?
Although containers can be removed with removeChild, I don't see a way to destroy the portal itself.
Any clues?
I'm trying to use portals in a virtual scroller (long story), so there's quite an active churn of portals.
I am trying to create multiple Portals but it doesn't seem to be working, is there any guide to achieving this?
Hi,I really appreciate your work, it soved my problem, but it does not full up the parent of the component, what I did was just add a line 61, the code looks like this:
if (elementType === ELEMENT_TYPE_HTML) {
element= document.createElement('div');
element.style.cssText = 'width:100%;height:100%;';
}
I do not know whether I have changed your code in the right way or in a wrong way, looking forward to your reply,
with many thanks!
If you have a text field in a portal, the autoFocus
attribute doesn't work. The text field isn't automatically focused.
Is this a bug, or is this expected to happen? In the latter case, is there a workaround?
https://codesandbox.io/s/wonderful-wilbur-4niwtl?file=/src/App.js
import {createHtmlPortalNode, InPortal, OutPortal} from 'react-reverse-portal';
export default function App() {
const node = createHtmlPortalNode();
return (
<div className="App">
<InPortal node={node}>
<input type="text" autoFocus />
</InPortal>
<OutPortal node={node} />
</div>
);
}
Hey @pimterry, came across this lib in the comments of facebook/react#13044, and it has been just the thing we needed to get react-beautiful-dnd draggables working from within other react component trees. So thanks for taking the time to put this together. ๐
Anyway, as unfortunate as it is, our builds need to support IE11 for just a little while longer and so we need an ES5 distribution to do that.
I'm not sure if this is something you're open to including as it adds additional bloat and very few people need it, but I figured I'd ask. I've got this working over on my fork (spong@bddc939) and am happy to open a PR, but just wanted to create an issue to get your feedback first.
If this isn't something you're interested in supporting, no worries at all. We can add the appropriate attribution and build from source on our end in this interim period while we still have to support IE11.
Thanks again!
You can see here: https://codesandbox.io/s/vigilant-platform-xy6onk?file=/src/index.tsx
Expected: scroll position maintained when switching outportals
Actual: scroll position resets to top
Error:
ReferenceError: document is not defined
It looks like the OutPortal
wraps the content in a <div>
, which results in the content not actually being rendered if it is used within a <svg>
since div
s cant exist in svg
s.
It would be nice if you could specify an optional component
prop or something to be used a wrapper for this render. If it was wrapped in a <g>
for example, I imagine it would work fine.
Example that doesn't actually work:
import React, { FC, useMemo } from "react";
import { createPortalNode, InPortal, OutPortal } from "react-reverse-portal";
const Test: FC = () => {
const portal = useMemo(() => createPortalNode(), []);
return (
<div>
<svg>
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
<svg x={30} y={10}>
<InPortal node={portal}>
<text alignmentBaseline="text-before-edge" fill="red">
test
</text>
</InPortal>
</svg>
<svg x={30} y={70}>
<OutPortal node={portal} />
</svg>
</svg>
</div>
);
};
export default Test;
If I have some eg onClick
events on component inside portal - this event will not be detected if used with reverse portal.
Eg. InPortal is rendered at the root level of the app
Then OutPortal is used inside some container that has it's own click events.
In such case - click events are only passed up directly to the root element, skipping container of OutPortal
.
Hi, I want to use this library to be able to move some components in a layout, without having to unmount/remount them. However, I am unable to do so because of race conditions when swapping nodes betwing two <OutPortal/>
s within a single rerender.
Here is a very simplified example that shows what happens:
Suppose I have two nodes NodeA and NodeB created once and for all.
Now suppose down the line we use those nodes in a layout, that would look like this at the first render.
<OutPortal node={nodeA}/> // let's call it OutPortal1, and its placeholder Placeholder1
<OutPortal node={nodeB}/> // let's call it OutPortal2, and its placeholder Placeholder2
Then, suppose an update arrives, yielding the following instead:
// nodeA and nodeB have been swapped.
<OutPortal node={nodeB}/>
<OutPortal node={nodeA}/>
Here is roughly what happens when going through the code of react-reverse-portal
:
OutPortal1 componentDidUpdate is triggered, with nodeB instead of nodeA
OutPortalB componentDidUpdate is triggered with nodeA instead of nodeB
Because of this race condition, we end up having OutPortal1 yielding Placeholder1 and OutPortal2 yielding NodeA, even though we just wanted to swap the nodes.
Note: this is a very simplified example, but my real use-case involves a recursive layout with potentially many content nodes.
Are there any plans on handling this use case ? Can you think of a workaround ?
I noticed all of the examples are limited to toggling between portals within the same component. Unfortunately if you swap portals that are within child components, you get unmounts every time you swap. Is there any way around this?
Thanks for sharing this awesome library!
Here is my code
<div>
<portals.InPortal node={colorPortalNode}>
<ColorProvider />
</portals.InPortal>
<portals.InPortal node={counterPortalNode}>
<CounterProvider />
</portals.InPortal>
<portals.OutPortal node={colorPortalNode}>
<portals.OutPortal node={counterPortalNode}>
<button onClick={() => setPage((prev) => (prev + 1) % pages.length)}>
Change Page
</button>
{pages[page] === "counter" && <Counter />}
{pages[page] === "color" && <Color />}
</portals.OutPortal>
</portals.OutPortal>
</div>
It seems only the inner portals.OutPortal
takes effect whereas the outer doesn't.
Aka:
CounterProvider
and ColorProvider
CounterProvider
Here is the codesandbox of the problem
I have been using this library with React and it has been extremely helpful and I haven't encountered any problems with it. Recently I started working on a project in Preact and I can't seem to get the portals to work properly.
I am getting the following errors:
src/app.tsx:23:11 - error TS2607: JSX element class does not support attributes because it does not have a 'props' property.
23 <portals.OutPortal node={portalNode}></portals.OutPortal>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/app.tsx:23:12 - error TS2786: 'portals.OutPortal' cannot be used as a JSX component.
Its instance type 'OutPortal<unknown>' is not a valid JSX element.
Type 'OutPortal<unknown>' is missing the following properties from type 'Component<any, any>': state, props, context, setState, forceUpdate
23 <portals.OutPortal node={portalNode}></portals.OutPortal>
~~~~~~~~~~~~~~~~~
src/ThruModal.tsx:49:7 - error TS2607: JSX element class does not support attributes because it does not have a 'props' property.
49 <portals.InPortal node={portalNode}>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ThruModal.tsx:49:8 - error TS2786: 'portals.InPortal' cannot be used as a JSX component.
Its instance type 'InPortal' is not a valid JSX element.
Type 'InPortal' is missing the following properties from type 'Component<any, any>': state, props, context, setState, forceUpdate
49 <portals.InPortal node={portalNode}>
~~~~~~~~~~~~~~~~
Is this library compatible with Preact? I should mention I am using Vite / rollup to create my builds where in the past when using React I used react-scripts / web-pack.
I would like to
Math.random()
or Date.now()
) andAnd for these purposes, it seems like this package doesn't do what I expected.
Here's the MWE example: CodeSandbox Link
//Box.tsx
function Box({ txt }: BoxProps) {
return (
<div style={{ border: "1px solid red" }}>
<strong>{txt}</strong>
<br />
{Date.now()}
</div>
);
}
export const MemoBox = memo(Box);
//App.tsx
import { MemoBox } from "./Box";
function App() {
const [show, setShow] = useState(true);
const node1 = useMemo(createHtmlPortalNode, []);
const node2 = useMemo(createHtmlPortalNode, []);
const toggle = useCallback(() => { setShow((show) => !show); }, []);
return (
<>
<InPortal node={node1}> <MemoBox txt="111" /> </InPortal>
<InPortal node={node2}> <MemoBox txt="222" /> </InPortal>
<button onClick={toggle}>Click me!</button>
<div>
{show && <OutPortal node={node1} />}
{show && <OutPortal node={node2} />}
</div>
<div>
{!show && <OutPortal node={node1} />}
</div>
</>
);
}
Every time I toggle, Box "111" alters its position and Box "222" repeats hiding and showing. Now let's focus on the timestamp.
Box "111" prints with the fixed timestamp, because every render exposes either OutPortal node1. This is fine.
Box "222" however, the timestamp has changed on every show, and this behavior is not quite intuitive when the package description says:
Rendering to zero OutPortals is fine: the node will be rendered as normal, using just the props provided inside the InPortal definition, and will continue to happily exist but just not appear in the DOM anywhere.
rather, it looks like when OutPortal node2 is absent, the detached node2 has been garbage collected and previous timestamp is gone forever.
I'm aware that below solutions will fix Box "222" timestamp as well, so that my purposes are satisfied:
Box
implementation (namely using useEffect
and useState
) to store the timestamp right after mountingdisplay:none
div.yet I'm curious why did the timestamp change when the component is supposed to be memoized - given that without these portals React.memo()
will do its work and fix the whole results.
is that normal?
return <div ref = { shellRef } data-type = 'cellshell' data-scrollerid = {scrollerID} data-index = {index} data-instanceid = {instanceID} style = {styles}>
{ ((cellStatus == 'render') || (cellStatus == 'renderplaceholder')) && <OutPortal node = {reverseportal} /> }
{ (cellStatus == 'render1') && <OutPortal node = {reverseportal} /> }
</div>
Both 'render' and 'render1' cause rendering of InPortal content (the console message is logged each time):
const GenericItem = (props) => {
const [genericstate, setGenericState] = useState('setup')
useEffect(()=>{
if (genericstate == 'setup') {
setGenericState('final')
}
},[genericstate])
console.log('rendering generic item index, genericstate',props.index,genericstate)
return <div style = {{position:'relative',height:'100%', width:'100%',backgroundColor:'white'}}>
<div style = {genericstyle}>
{props.index + 1}{false && <img style= {{height:'100%'}} src={props.image}/>}
</div>
</div>
}
... but state ('final') is preserved
I was hoping that the move wouldn't trigger a render at all.
Hello, is there a way to differ the first render of the InPortal until the first outPortal is rendered? I want to prevent useless render / enable lazy loading for scenarios where the OutPortal would not be used in the user's navigation.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.