From da000bcce252b478994b6e26ef8c5d36da051601 Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 6 Jul 2026 12:36:44 +0500 Subject: [PATCH 1/2] Fix editor lifecycle bugs in EmailEditor - The unmount cleanup captured `editor` from the first render (always null), so unlayer editors were never destroyed on unmount. Track the latest instance in a ref so cleanup destroys the real editor. - The event-listener effect depended on Object.keys(methodProps) which yields array indices ("0,1,2"), not prop names, so swapping one on* prop for another was not detected. Use methodProps.join(',') instead. Adds a regression test that mounts and unmounts the editor with a mocked embed script and asserts destroy() is called. --- src/EmailEditor.tsx | 10 ++++++++-- test/destroy.test.tsx | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 test/destroy.test.tsx diff --git a/src/EmailEditor.tsx b/src/EmailEditor.tsx index 52316273e..7bb64591c 100644 --- a/src/EmailEditor.tsx +++ b/src/EmailEditor.tsx @@ -3,6 +3,7 @@ import React, { useState, useImperativeHandle, useMemo, + useRef, } from 'react'; import type { DisplayMode, UnlayerEditor } from '@unlayer/types'; @@ -51,9 +52,14 @@ function EmailEditorInner { return () => { - editor?.destroy(); + editorRef.current?.destroy(); }; }, []); @@ -93,7 +99,7 @@ function EmailEditorInner ({ + loadScript: (callback: Function) => callback(), +})); + +it('destroys the editor on unmount', () => { + const destroy = jest.fn(); + (global as any).unlayer = { + createEditor: jest.fn(() => ({ + destroy, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })), + }; + + const { unmount } = render(); + unmount(); + + expect(destroy).toHaveBeenCalledTimes(1); +}); From 650a2abb6d26ebd1fa3cb875dece10b4c20ac4aa Mon Sep 17 00:00:00 2001 From: farhan Date: Mon, 6 Jul 2026 12:48:37 +0500 Subject: [PATCH 2/2] Add regression test for unmount before embed script loads Covers the scenario from #272: a component that is mounted and unmounted quickly, with the embed script finishing its load afterwards. Asserts no editor is created for the unmounted component and the late script callback does not throw. --- test/quickUnmount.test.tsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/quickUnmount.test.tsx diff --git a/test/quickUnmount.test.tsx b/test/quickUnmount.test.tsx new file mode 100644 index 000000000..7615c929f --- /dev/null +++ b/test/quickUnmount.test.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import Editor from '../src'; + +let scriptLoadedCallback: Function; +jest.mock('../src/loadScript', () => ({ + loadScript: (callback: Function) => { + scriptLoadedCallback = callback; + }, +})); + +it('does not create the editor when unmounted before the embed script loads', () => { + const createEditor = jest.fn(); + (global as any).unlayer = { createEditor }; + + const { unmount } = render(); + unmount(); + + // Embed script finishes loading after the component is gone. + act(() => { + scriptLoadedCallback(); + }); + + expect(createEditor).not.toHaveBeenCalled(); +});