Skip to content
Merged
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
77 changes: 76 additions & 1 deletion src/__tests__/react-plotly.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jest-environment jsdom */
import React, {useState} from 'react';
import React, {StrictMode, useState} from 'react';
import {act, render} from '@testing-library/react';
import createComponent from '../factory';
import once from 'onetime';
Expand Down Expand Up @@ -204,5 +204,80 @@ describe('<Plotly/>', () => {
.catch((err) => done(err));
});
});

describe('StrictMode', () => {
// Regression: in dev StrictMode, React runs effects setup-cleanup-setup
// to surface missing cleanup. Our cleanup calls Plotly.purge, so the
// simulated re-setup must re-initialize. Without resetting prevRef in
// cleanup, the mount/update effect skips re-init and the chart is dead.
test('re-initializes plot after simulated remount', (done) => {
Plotly.react.mockClear();
Plotly.purge.mockClear();

let initCount = 0;
render(
<StrictMode>
<PlotComponent
data={[{x: [1, 2, 3]}]}
onInitialized={() => {
initCount++;
}}
onError={(err) => done(err)}
/>
</StrictMode>
);

setTimeout(() => {
try {
// Purge ran (StrictMode simulated unmount). React must run again
// afterwards to bring the plot back.
expect(Plotly.purge).toHaveBeenCalledTimes(1);
expect(Plotly.react.mock.calls.length).toBeGreaterThan(Plotly.purge.mock.calls.length);
expect(initCount).toBeGreaterThanOrEqual(1);
done();
} catch (e) {
done(e);
}
}, 50);
});
});

describe('unmount', () => {
// Regression: React detaches callback refs before useEffect cleanups run,
// so reading the ref from cleanup sees `null`. The cleanup effect must
// capture the element at setup time so onPurge/Plotly.purge still fire.
test('fires onPurge and Plotly.purge on unmount', (done) => {
const purgeCalls = [];
let gd;
let resolveInit;
const initialized = new Promise((resolve) => {
resolveInit = resolve;
});

const result = render(
<PlotComponent
data={[{x: [1, 2, 3]}]}
ref={(el) => {
gd = el;
}}
onPurge={(figure, el) => purgeCalls.push({figure, gd: el})}
onInitialized={once(resolveInit)}
onError={(err) => done(err)}
/>
);

initialized
.then(() => {
// Capture before unmount — our ref callback nulls `gd` on detach.
const capturedGd = gd;
act(() => result.unmount());
expect(Plotly.purge).toHaveBeenCalledWith(capturedGd);
expect(purgeCalls).toHaveLength(1);
expect(purgeCalls[0].gd).toBe(capturedGd);
done();
})
.catch(done);
});
});
});
});
6 changes: 5 additions & 1 deletion src/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ export default function plotComponentFactory(Plotly) {

// Cleanup effect — runs on unmount only.
useEffect(() => {
const el = elRef.current;
return () => {
unmountingRef.current = true;
const el = elRef.current;
if (el) {
if (typeof onPurgeRef.current === 'function') {
const frames = el._transitionData ? el._transitionData._frames : null;
Expand All @@ -178,6 +178,10 @@ export default function plotComponentFactory(Plotly) {
window.removeEventListener('resize', resizeHandlerRef.current);
resizeHandlerRef.current = null;
}
// Reset refs so StrictMode's re-setup looks like a fresh mount
prevRef.current = null;
promiseRef.current = Promise.resolve();
handlersRef.current = {};
};
}, []);

Expand Down
Loading