From 72e77f14dd32d9af5afe09bfb4a87af421ebe446 Mon Sep 17 00:00:00 2001 From: Ferit Date: Sat, 24 Oct 2026 23:15:33 +0300 Subject: [PATCH 1/2] fix: cancel queued batch updates when unmounted in useBatcher --- src/hooks/useEffectState.tsx | 14 +++++++++++++- tests/useEffectState.spec.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/useEffectState.spec.tsx diff --git a/src/hooks/useEffectState.tsx b/src/hooks/useEffectState.tsx index 75d47bc..d88a66f 100644 --- a/src/hooks/useEffectState.tsx +++ b/src/hooks/useEffectState.tsx @@ -15,15 +15,27 @@ type NotifyEffectUpdate = (callback: UpdateCallbackFunc) => void; export function useBatcher() { // Updater Trigger const updateFuncRef = React.useRef(null); + const destroyedRef = React.useRef(false); + + React.useEffect(() => { + return () => { + destroyedRef.current = true; + updateFuncRef.current = null; + }; + }, []); // Notify update const notifyEffectUpdate: NotifyEffectUpdate = callback => { + if (destroyedRef.current) { + return; + } + if (!updateFuncRef.current) { updateFuncRef.current = []; channelUpdate(() => { unstable_batchedUpdates(() => { - updateFuncRef.current.forEach(fn => { + updateFuncRef.current?.forEach(fn => { fn(); }); updateFuncRef.current = null; diff --git a/tests/useEffectState.spec.tsx b/tests/useEffectState.spec.tsx new file mode 100644 index 0000000..d3c4a4d --- /dev/null +++ b/tests/useEffectState.spec.tsx @@ -0,0 +1,24 @@ +import { act, renderHook } from '@testing-library/react'; +import { useBatcher } from '../src/hooks/useEffectState'; + +describe('useEffectState / useBatcher', () => { + it('should clean up queued updates on unmount', async () => { + let notify: any; + let updateCalled = false; + const { unmount } = renderHook(() => { + notify = useBatcher(); + }); + + notify(() => { + updateCalled = true; + }); + + unmount(); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + }); + + expect(updateCalled).toBe(false); + }); +}); From 9c94f55a09a97f5ee2e4f6759e0bd50ad99775fb Mon Sep 17 00:00:00 2001 From: Ferit Date: Tue, 27 Oct 2026 01:29:48 +0200 Subject: [PATCH 2/2] fix: restore batch updates after StrictMode effect replay --- src/hooks/useEffectState.tsx | 1 + tests/useEffectState.spec.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/hooks/useEffectState.tsx b/src/hooks/useEffectState.tsx index d88a66f..9643a70 100644 --- a/src/hooks/useEffectState.tsx +++ b/src/hooks/useEffectState.tsx @@ -18,6 +18,7 @@ export function useBatcher() { const destroyedRef = React.useRef(false); React.useEffect(() => { + destroyedRef.current = false; return () => { destroyedRef.current = true; updateFuncRef.current = null; diff --git a/tests/useEffectState.spec.tsx b/tests/useEffectState.spec.tsx index d3c4a4d..dc2f1c9 100644 --- a/tests/useEffectState.spec.tsx +++ b/tests/useEffectState.spec.tsx @@ -2,6 +2,21 @@ import { act, renderHook } from '@testing-library/react'; import { useBatcher } from '../src/hooks/useEffectState'; describe('useEffectState / useBatcher', () => { + it('should process updates after StrictMode re-establishes effects', async () => { + const callback = jest.fn(); + const { result } = renderHook(() => useBatcher(), { + reactStrictMode: true, + }); + + result.current(callback); + + await act(async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + it('should clean up queued updates on unmount', async () => { let notify: any; let updateCalled = false;