diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js
index 070aca1..bace500 100644
--- a/src/__tests__/react-plotly.test.js
+++ b/src/__tests__/react-plotly.test.js
@@ -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';
@@ -204,5 +204,80 @@ describe('', () => {
.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(
+
+ {
+ initCount++;
+ }}
+ onError={(err) => done(err)}
+ />
+
+ );
+
+ 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(
+ {
+ 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);
+ });
+ });
});
});
diff --git a/src/factory.js b/src/factory.js
index 6f88955..99c4860 100644
--- a/src/factory.js
+++ b/src/factory.js
@@ -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;
@@ -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 = {};
};
}, []);