Skip to content

Commit b85ad85

Browse files
committed
feat(useImmediateUpdateEffect): allow teardown from last effect like builtin effect hooks
1 parent 76b18d0 commit b85ad85

File tree

4 files changed

+83
-5
lines changed

4 files changed

+83
-5
lines changed

src/useImmediateUpdateEffect.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { DependencyList, useRef } from 'react'
1+
import { DependencyList, useRef, EffectCallback } from 'react'
22
import useStableMemo from './useStableMemo'
3+
import useWillUnmount from './useWillUnmount'
34

45
/**
56
* An _immediate_ effect that runs an effect callback when its dependency array
@@ -17,16 +18,25 @@ import useStableMemo from './useStableMemo'
1718
*
1819
* @category effects
1920
*/
20-
function useImmediateUpdateEffect(effect: () => void, deps: DependencyList) {
21+
function useImmediateUpdateEffect(
22+
effect: EffectCallback,
23+
deps: DependencyList,
24+
) {
2125
const firstRef = useRef(true)
26+
const tearDown = useRef<ReturnType<EffectCallback>>()
27+
28+
useWillUnmount(() => {
29+
if (tearDown.current) tearDown.current()
30+
})
2231

2332
useStableMemo(() => {
2433
if (firstRef.current) {
2534
firstRef.current = false
2635
return
2736
}
2837

29-
effect()
38+
if (tearDown.current) tearDown.current()
39+
tearDown.current = effect()
3040
}, deps)
3141
}
3242

src/useStableMemo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DependencyList, useEffect, useRef } from 'react'
1+
import { DependencyList, useRef } from 'react'
22

33
function isEqual(a: DependencyList, b: DependencyList) {
44
if (a.length !== b.length) return false

test/useCustomEffect.test.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import useCustomEffect from '../src/useCustomEffect'
2+
import useImmediateUpdateEffect from '../src/useImmediateUpdateEffect'
3+
import { renderHook } from './helpers'
4+
5+
describe('useCustomEffect', () => {
6+
it('should run custom isEqual logic', () => {
7+
const teardown = jest.fn()
8+
9+
const spy = jest.fn().mockImplementation(() => teardown)
10+
const isEqual = jest.fn((next, prev) => next[0].foo === prev[0].foo)
11+
12+
const [, wrapper] = renderHook(
13+
({ value }) => {
14+
useCustomEffect(spy, [value], isEqual)
15+
},
16+
{ value: { foo: true } },
17+
)
18+
19+
expect(spy).toHaveBeenCalledTimes(1)
20+
21+
// matches isEqual
22+
wrapper.setProps({ value: { foo: true } })
23+
24+
expect(spy).toHaveBeenCalledTimes(1)
25+
26+
// update that should trigger
27+
wrapper.setProps({ value: { foo: false } })
28+
29+
expect(spy).toHaveBeenCalledTimes(2)
30+
expect(isEqual).toHaveBeenCalledTimes(2)
31+
32+
expect(teardown).toBeCalledTimes(1)
33+
expect(spy).toHaveBeenCalledTimes(2)
34+
35+
wrapper.unmount()
36+
expect(teardown).toBeCalledTimes(2)
37+
})
38+
39+
it('should accept different hooks', () => {
40+
const spy = jest.fn()
41+
const hookSpy = jest.fn().mockImplementation(useImmediateUpdateEffect)
42+
43+
const [, wrapper] = renderHook(
44+
({ value }) => {
45+
useCustomEffect(spy, [value], {
46+
isEqual: (next, prev) => next[0].foo === prev[0].foo,
47+
effectHook: hookSpy,
48+
})
49+
},
50+
{ value: { foo: true } },
51+
)
52+
53+
expect(hookSpy).toHaveBeenCalledTimes(1)
54+
// not called b/c useImmediateUpdateEffect doesn't run on initial render
55+
expect(spy).toHaveBeenCalledTimes(0)
56+
})
57+
})

test/useImmediateUpdateEffect.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { renderHook } from './helpers'
33

44
describe('useImmediateUpdateEffect', () => {
55
it('should run update after value changes', () => {
6-
const spy = jest.fn()
6+
const teardown = jest.fn()
7+
const spy = jest.fn().mockImplementation(() => teardown)
78

89
const [, wrapper] = renderHook(
910
({ value }) => {
@@ -18,8 +19,18 @@ describe('useImmediateUpdateEffect', () => {
1819

1920
expect(spy).toHaveBeenCalledTimes(1)
2021

22+
// update that doesn't change the deps Array
2123
wrapper.setProps({ value: 2, other: true })
2224

2325
expect(spy).toHaveBeenCalledTimes(1)
26+
27+
// second update
28+
wrapper.setProps({ value: 4, other: true })
29+
30+
expect(teardown).toBeCalledTimes(1)
31+
expect(spy).toHaveBeenCalledTimes(2)
32+
33+
wrapper.unmount()
34+
expect(teardown).toBeCalledTimes(2)
2435
})
2536
})

0 commit comments

Comments
 (0)