diff --git a/.changeset/cool-spies-leave.md b/.changeset/cool-spies-leave.md new file mode 100644 index 00000000..dda4e6cf --- /dev/null +++ b/.changeset/cool-spies-leave.md @@ -0,0 +1,5 @@ +--- +'react-simplikit': patch +--- + +fix(core/hooks): call cleanup when unmount occurs before async effect resolves diff --git a/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.spec.ts b/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.spec.ts index 9f9cf5b0..4d81d1f0 100644 --- a/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.spec.ts +++ b/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.spec.ts @@ -89,6 +89,25 @@ describe('useAsyncEffect', () => { expect(cleanup).toHaveBeenCalled(); }); + it('should call cleanup even when component unmounts before async effect resolves', async () => { + const cleanup = vi.fn(); + const { unmount } = await renderHookSSR(() => + useAsyncEffect(async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + return cleanup; + }, []) + ); + + await flushPromises(); + unmount(); + expect(cleanup).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(1000); + await flushPromises(); + + expect(cleanup).toHaveBeenCalledTimes(1); + }); + it('should call effect every rerender when deps are undefined', async () => { const effect = vi.fn().mockResolvedValue(undefined); diff --git a/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.ts b/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.ts index 7fade4bc..6394359a 100644 --- a/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.ts +++ b/packages/core/src/hooks/useAsyncEffect/useAsyncEffect.ts @@ -24,12 +24,17 @@ import { DependencyList, useEffect } from 'react'; export function useAsyncEffect(effect: () => Promise void)>, deps?: DependencyList) { useEffect(() => { let cleanup: (() => void) | void; + let isCleaned = false; effect().then(result => { cleanup = result; + if (isCleaned) { + cleanup?.(); + } }); return () => { + isCleaned = true; cleanup?.(); };