Skip to content

Commit c849a4c

Browse files
committed
Orchestrate InPortal/OutPortal render order via a callbackFn
1 parent 95252b0 commit c849a4c

File tree

2 files changed

+65
-20
lines changed

2 files changed

+65
-20
lines changed

src/index.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type PortalNodeElement = HTMLElement | SVGElement;
1414
export interface PortalNode<C extends Component<any> = Component<any>> {
1515
// The dom element used for the React portal. Created on portal mount.
1616
element: PortalNodeElement | null,
17+
callbackFn: (() => void) | null,
1718
// Used by the out portal to send props back to the real element
1819
// Hooked by InPortal to become a state update (and thus rerender)
1920
setPortalProps(p: ComponentProps<C>): void;
@@ -40,6 +41,7 @@ export const createPortalNode = <C extends Component<any>>(): PortalNode<C> => {
4041

4142
const portalNode: PortalNode = {
4243
element: null,
44+
callbackFn: null,
4345
setPortalProps: (props: ComponentProps<C>) => {
4446
initialProps = props;
4547
},
@@ -93,13 +95,13 @@ export const createPortalNode = <C extends Component<any>>(): PortalNode<C> => {
9395
return portalNode;
9496
};
9597

96-
export class InPortal extends React.PureComponent<InPortalProps, { nodeProps: {}, isFirstRender: boolean }> {
98+
export class InPortal extends React.PureComponent<InPortalProps, { nodeProps: {}, outPortalCallback: boolean }> {
9799

98100
constructor(props: InPortalProps) {
99101
super(props);
100102
this.state = {
101103
nodeProps: this.props.node.getInitialPortalProps(),
102-
isFirstRender: true,
104+
outPortalCallback: false,
103105
};
104106
}
105107

@@ -123,10 +125,11 @@ export class InPortal extends React.PureComponent<InPortalProps, { nodeProps: {}
123125
render() {
124126
const { children, node } = this.props;
125127

126-
if (!node.element && this.state.isFirstRender) {
127-
// If the InPortal is rendered before the OutPortal then the portal element won't exist yet:
128-
// wait for it to initialize and then try again
129-
setTimeout(() => this.setState({ isFirstRender: false }))
128+
if (!node.element) {
129+
// The OutPortal has not yet determined whether or not we're in a special namespace like SVG:
130+
// delay our rendering for now, and attach a callbackFn which the OutPortal can use to
131+
// trigger a rerender once we're ready
132+
node.callbackFn = () => this.setState({ outPortalCallback: true })
130133
return null;
131134
}
132135

@@ -157,6 +160,13 @@ export class OutPortal<C extends Component<any>> extends React.PureComponent<Out
157160
passPropsThroughPortal() {
158161
const propsForTarget = Object.assign({}, this.props, { node: undefined });
159162
this.props.node.setPortalProps(propsForTarget);
163+
164+
if (this.props.node.callbackFn) {
165+
// There's an InPortal which is waiting on this OutPortal:
166+
// rerender it now that the OutPortal and portalNode are ready
167+
this.props.node.callbackFn();
168+
this.props.node.callbackFn = null;
169+
}
160170
}
161171

162172
componentDidMount() {

stories/index.stories.js

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,32 @@ const Container = (props) =>
1111

1212
storiesOf('Portals', module)
1313
.add('render things in different places', () => {
14-
const portalNode = portals.createPortalNode();
14+
const portalNode1 = portals.createPortalNode();
15+
const portalNode2 = portals.createPortalNode();
1516

1617
return <div>
1718
<div>
18-
Portal defined here:
19-
<portals.InPortal node={portalNode}>
19+
Portal 1 defined here:
20+
<portals.InPortal node={portalNode1}>
2021
Hi!
2122
</portals.InPortal>
2223
</div>
2324

2425
<div>
25-
Portal renders here:
26-
<portals.OutPortal node={portalNode} />
26+
Portal 1 renders here:
27+
<portals.OutPortal node={portalNode1} />
28+
</div>
29+
30+
<div>
31+
Portal 2 renders here:
32+
<portals.OutPortal node={portalNode2} />
33+
</div>
34+
35+
<div>
36+
Portal 2 defined here:
37+
<portals.InPortal node={portalNode2}>
38+
Hi!
39+
</portals.InPortal>
2740
</div>
2841
</div>;
2942
})
@@ -227,29 +240,49 @@ storiesOf('Portals', module)
227240
</div>;
228241
})
229242
.add('works with SVGs', () => {
230-
const portalNode = portals.createPortalNode();
243+
const portalNode1 = portals.createPortalNode();
244+
const portalNode2 = portals.createPortalNode();
231245

232246
// From https://github.com/httptoolkit/react-reverse-portal/issues/2
233247
return <div>
248+
<div>
234249
<svg>
235250
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
236251
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
237252
<svg x={30} y={10}>
238-
<portals.InPortal node={portalNode}>
253+
<portals.InPortal node={portalNode1}>
239254
<text alignmentBaseline="text-before-edge" dominantBaseline="hanging" fill="red">
240255
test
241256
</text>
242257
</portals.InPortal>
243258
</svg>
244259
<svg x={30} y={70}>
245-
<portals.OutPortal node={portalNode} />
260+
<portals.OutPortal node={portalNode1} />
246261
</svg>
247262
</svg>
263+
264+
</div>
265+
<div>
266+
<svg>
267+
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
268+
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
269+
<svg x={30} y={70}>
270+
<portals.OutPortal node={portalNode2} />
271+
</svg>
272+
<svg x={30} y={10}>
273+
<portals.InPortal node={portalNode2}>
274+
<text alignmentBaseline="text-before-edge" dominantBaseline="hanging" fill="red">
275+
test
276+
</text>
277+
</portals.InPortal>
278+
</svg>
279+
</svg>
280+
</div>
248281
</div>
249282

250283
})
251284
.add('can move content around within SVGs', () => {
252-
const portalNode = portals.createPortalNode('text');
285+
const portalNode = portals.createPortalNode();
253286

254287
return React.createElement(() => {
255288
const [inFirstSvg, setSvgToUse] = React.useState(false);
@@ -290,23 +323,25 @@ storiesOf('Portals', module)
290323
Click to move the OutPortal within the SVG
291324
</button>
292325

293-
<svg>
326+
<div>
327+
<svg width={600} height={800}>
294328
<portals.InPortal node={portalNode}>
295-
<foreignObject width="300" height="50">
329+
<foreignObject width="500" height="400">
296330
<video src="https://media.giphy.com/media/l0HlKghz8IvrQ8TYY/giphy.mp4" controls loop />
297331
</foreignObject>
298332
</portals.InPortal>
299333

300-
<rect x={0} y={0} width={300} height={50} fill="gray"></rect>
301-
<rect x={0} y={50} width={300} height={50} fill="lightblue"></rect>
334+
<rect x={0} y={0} width={600} height={400} fill="gray"></rect>
335+
<rect x={0} y={400} width={600} height={400} fill="lightblue"></rect>
302336

303337
<svg x={30} y={10}>
304338
{ inFirstSvg && <portals.OutPortal node={portalNode} /> }
305339
</svg>
306-
<svg x={30} y={70}>
340+
<svg x={30} y={410}>
307341
{ !inFirstSvg && <portals.OutPortal node={portalNode} /> }
308342
</svg>
309343
</svg>
344+
</div>
310345
</div>
311346
})
312347
})

0 commit comments

Comments
 (0)