Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/Portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,17 @@ const Portal = React.forwardRef<any, PortalProps>((props, ref) => {
>(() => getPortalContainer(getContainer));

React.useEffect(() => {
const customizeContainer = getPortalContainer(getContainer);

// Tell component that we check this in effect which is safe to be `null`
setInnerContainer(
() =>
// React do the state update even the value is the same,
// Use function call to force React to compare update
customizeContainer ?? null,
);
const customizeContainer = getPortalContainer(getContainer) ?? null;

// Re-resolved after every render — a stable `getContainer` may close over a
// ref whose element changes — but only a changed container is stored. React
// skips a same-value update only while this fiber has no pending lane; once
// one lingers, every no-op call schedules a render, and a parent re-rendering
// on each keystroke stacks those renders into "Maximum update depth exceeded".
if (customizeContainer !== innerContainer) {
setInnerContainer(customizeContainer);
}
});

const [defaultContainer, queueCreate] = useDom(
Expand Down
43 changes: 43 additions & 0 deletions tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,49 @@ describe('Portal', () => {
expect(container).toMatchSnapshot();
expect(renderTimes).toEqual(2);
});

it('switch container without a redundant render', () => {
// The container is re-resolved after every render, but only a changed
// one may be stored: React skips a same-value update only while the fiber
// has no pending lane, so a no-op right after a real update costs a
// render, and under rapid parent re-renders those renders compound into
// "Maximum update depth exceeded" (#27).
const portal = Portal as unknown as {
render: (...args: unknown[]) => React.ReactNode;
};
const originalRender = portal.render;
let portalRenders = 0;
portal.render = (...args) => {
portalRenders += 1;
return originalRender(...args);
};

const first = document.createElement('div');
const second = document.createElement('div');
document.body.append(first, second);

const Demo = ({ target }: { target: HTMLElement }) => (
<Portal open getContainer={() => target}>
<span className="switch-content" />
</Portal>
);

try {
const { rerender } = render(<Demo target={first} />);
expect(first.querySelector('.switch-content')).toBeTruthy();

portalRenders = 0;
rerender(<Demo target={second} />);

expect(second.querySelector('.switch-content')).toBeTruthy();
// The parent's render, plus the one storing the new container.
expect(portalRenders).toEqual(2);
} finally {
portal.render = originalRender;
first.remove();
second.remove();
}
});
});

describe('dynamic change autoLock', () => {
Expand Down