Skip to content

Commit 49e51ec

Browse files
committed
fix teardowns
1 parent cfa52dc commit 49e51ec

File tree

3 files changed

+42
-8
lines changed

3 files changed

+42
-8
lines changed

src/useCustomEffect.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { DependencyList, EffectCallback, useRef, useEffect } from 'react'
1+
import {
2+
DependencyList,
3+
EffectCallback,
4+
useRef,
5+
useEffect,
6+
useDebugValue,
7+
} from 'react'
8+
import useWillUnmount from './useWillUnmount'
9+
import useMounted from './useMounted'
210

311
export type EffectHook = (effect: EffectCallback, deps?: DependencyList) => void
412

@@ -9,6 +17,11 @@ export type CustomEffectOptions<TDeps> = {
917
effectHook?: EffectHook
1018
}
1119

20+
type CleanUp = {
21+
(): void
22+
cleanup?: ReturnType<EffectCallback>
23+
}
24+
1225
/**
1326
* a useEffect() hook with customized depedency comparision
1427
*
@@ -40,20 +53,39 @@ function useCustomEffect<TDeps extends DependencyList = DependencyList>(
4053
dependencies: TDeps,
4154
isEqualOrOptions: IsEqual<TDeps> | CustomEffectOptions<TDeps>,
4255
) {
56+
const isMounted = useMounted()
4357
const { isEqual, effectHook = useEffect } =
4458
typeof isEqualOrOptions === 'function'
4559
? { isEqual: isEqualOrOptions }
4660
: isEqualOrOptions
4761

48-
const depsRef = useRef<TDeps | undefined>()
62+
const dependenciesRef = useRef<TDeps>()
63+
dependenciesRef.current = dependencies
64+
65+
const cleanupRef = useRef<CleanUp | null>(null)
66+
4967
effectHook(() => {
50-
const prev = depsRef.current
51-
depsRef.current = dependencies
68+
// If the ref the is `null` it's either the first effect or the last effect
69+
// ran and was cleared, meaning _this_ update should run, b/c the equality
70+
// check failed on in the cleanup of the last effect.
71+
if (cleanupRef.current === null) {
72+
const cleanup = effect()
5273

53-
if (!prev || !isEqual(dependencies, prev)) {
54-
return effect()
74+
cleanupRef.current = () => {
75+
if (isMounted() && isEqual(dependenciesRef.current!, dependencies)) {
76+
return
77+
}
78+
79+
cleanupRef.current = null
80+
if (cleanup) cleanup()
81+
}
82+
cleanupRef.current.cleanup = cleanup
5583
}
84+
85+
return cleanupRef.current ?? undefined
5686
})
87+
88+
useDebugValue(effect)
5789
}
5890

5991
export default useCustomEffect

test/useCustomEffect.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('useCustomEffect', () => {
4040
const spy = jest.fn()
4141
const hookSpy = jest.fn().mockImplementation(useImmediateUpdateEffect)
4242

43-
const [, wrapper] = renderHook(
43+
renderHook(
4444
({ value }) => {
4545
useCustomEffect(spy, [value], {
4646
isEqual: (next, prev) => next[0].foo === prev[0].foo,
@@ -50,6 +50,7 @@ describe('useCustomEffect', () => {
5050
{ value: { foo: true } },
5151
)
5252

53+
// the update and unmount hook setup
5354
expect(hookSpy).toHaveBeenCalledTimes(1)
5455
// not called b/c useImmediateUpdateEffect doesn't run on initial render
5556
expect(spy).toHaveBeenCalledTimes(0)

test/useMutationObserver.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('useMutationObserver', () => {
4444
disconnentSpy?.mockRestore()
4545
})
4646

47-
it.only('should update config', async () => {
47+
it('should update config', async () => {
4848
const teardown = jest.fn()
4949
const spy = jest.fn(() => teardown)
5050

@@ -82,6 +82,7 @@ describe('useMutationObserver', () => {
8282
],
8383
expect.anything(),
8484
)
85+
expect(disconnentSpy).toBeCalledTimes(1)
8586

8687
wrapper.unmount()
8788

0 commit comments

Comments
 (0)