Skip to content

Commit d0235bc

Browse files
committed
v1.3.0
- Add configurable options to hook - Add `cleanUpOnCall` option - Add `cleanUpOnDepsChange` option - Add `cleanUpOnUnmount` option
1 parent dfed81a commit d0235bc

File tree

4 files changed

+420
-26
lines changed

4 files changed

+420
-26
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ const result = greetAndAlert("world");
120120
// `result` is returned here, *and* we still get to use the cleanup!
121121
```
122122

123+
## Options
124+
`useCleanupCallback` takes an optional object as a third argument to pass additional options to in order to customize the behaviour of the hook.
125+
126+
| Name | Type | Default | Description |
127+
|---------------------|---------|---------|----------------------------------------------------------------------------------|
128+
| cleanUpOnCall | boolean | true | Whether to call the last cleanup when the output callback of the hook is called. |
129+
| cleanUpOnDepsChange | boolean | false | Whether to call the last cleanup when dependencies change. |
130+
| cleanUpOnUnmount | boolean | true | Whether to call the last cleanup when the hook unmounts. |
131+
132+
123133
## Limitations
124134

125135
- Similarly to `useEffect`, the `callback` and returned `cleanup callback` must be synchronous (i.e. an `async` function callback will not work). You can alleviate this the same way you would with `useEffect` by defining the asynchronous function within the callback, and calling it immediately.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "use-cleanup-callback",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"description": "React hook that is a hybrid of useCallback and the cleanup from useEffect",
55
"main": "dist/index.js",
66
"source": "src/index.ts",
@@ -11,8 +11,11 @@
1111
"scripts": {
1212
"build": "rimraf dist/* && microbundle && rimraf dist/*mjs* dist/*umd* dist/*test*",
1313
"dev": "microbundle watch",
14+
"lint": "eslint ./src/**",
1415
"test": "jest",
15-
"test-watch": "jest --watch"
16+
"test-types": "tsc --noEmit",
17+
"test-watch": "jest --watch",
18+
"checks": "eslint ./src/** && jest && tsc --noEmit"
1619
},
1720
"keywords": [
1821
"react",

src/index.ts

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,55 @@
11
import { useCallback, useEffect, DependencyList, useRef } from 'react';
22

3+
/**
4+
* A standard cleanup
5+
*/
36
export type Cleanup = void | (() => void | undefined);
47

8+
/**
9+
* An object notation cleanup used when a return value is required from the callback returned
10+
* by `useCleanupCallback`
11+
*/
512
export type ReturnObject<V> = {
613
value: V;
714
cleanup: Cleanup;
815
};
916

17+
/**
18+
* An input/output callback that returns a standard cleanup
19+
*/
1020
export type CleanupCallback<T extends unknown[]> = (...args: T) => Cleanup;
11-
export type CleanupCallbackWithReturn<T extends unknown[], V> = (
12-
...args: T
13-
) => ReturnObject<V>;
14-
export type ReturnedCleanupCallbackWithReturn<T extends unknown[], V> = (
15-
...args: T
16-
) => V;
17-
18-
function executeIfFunction(arg: unknown) {
19-
if (typeof arg === 'function') arg();
21+
22+
/**
23+
* An input callback that returns an object notation cleanup
24+
*/
25+
export type CleanupCallbackWithReturn<T extends unknown[], V> = (...args: T) => ReturnObject<V>;
26+
27+
/**
28+
* Additional `useCleanupCallback` options
29+
*/
30+
export type Options = {
31+
cleanUpOnCall?: boolean;
32+
cleanUpOnDepsChange?: boolean;
33+
cleanUpOnUnmount?: boolean;
34+
};
35+
36+
/**
37+
* The output callback returned from `useCleanupCallback` when an object notation cleanup is used.
38+
* Provides a callback that itself returns the desired value.
39+
*/
40+
export type ReturnedCleanupCallbackWithReturn<T extends unknown[], V> = (...args: T) => V;
41+
42+
/**
43+
* Handles calling a cleanup if it hasn't been called, and is a function
44+
*/
45+
function handleCleanupObj(cleanupObj: { cleanup: unknown; isCalled: boolean }): void {
46+
const { cleanup, isCalled } = cleanupObj;
47+
if (!isCalled && typeof cleanup === 'function') {
48+
// mark it as called
49+
cleanupObj.isCalled = true;
50+
// call the cleanup function
51+
cleanup();
52+
}
2053
}
2154

2255
/**
@@ -26,43 +59,108 @@ function executeIfFunction(arg: unknown) {
2659
* @param callback - Callback to be memoized. Supports returning a 'cleanup' callback.
2760
* @param deps - Dependency array for the associated callback.
2861
*/
62+
63+
/* overload 1 - no cleanup, or standard cleanup
64+
e.g.
65+
useCleanupCallback(() => {
66+
// some code
67+
return;
68+
}, []);
69+
70+
e.g.
71+
useCleanupCallback(() => {
72+
// some code
73+
return () => {
74+
// do cleanup
75+
};
76+
}, []);
77+
*/
2978
function useCleanupCallback<T extends unknown[]>(
3079
callback: CleanupCallback<T>,
31-
deps: DependencyList
80+
deps: DependencyList,
81+
options?: Options
3282
): CleanupCallback<T>;
83+
84+
/* overload 2 - object return notation (return value & cleanup)
85+
e.g.
86+
useCleanupCallback(() => {
87+
// some code
88+
return {
89+
value: 'foo',
90+
cleanup: () =>{
91+
// do cleanup
92+
}
93+
}
94+
}, [])
95+
*/
3396
function useCleanupCallback<T extends unknown[], V>(
3497
callback: CleanupCallbackWithReturn<T, V>,
35-
deps: DependencyList
98+
deps: DependencyList,
99+
options?: Options
36100
): ReturnedCleanupCallbackWithReturn<T, V>;
101+
102+
/* overload 3 - Union of overloads 1 & 2 */
37103
function useCleanupCallback<T extends unknown[], V>(
38104
callback: CleanupCallback<T> | CleanupCallbackWithReturn<T, V>,
39-
deps: DependencyList
105+
deps: DependencyList,
106+
options?: Options
40107
): CleanupCallback<T> | ReturnedCleanupCallbackWithReturn<T, V>;
108+
109+
/* implementation signature */
41110
function useCleanupCallback<T extends unknown[], V>(
42111
callback: CleanupCallback<T> | CleanupCallbackWithReturn<T, V>,
43-
deps: DependencyList
112+
deps: DependencyList,
113+
{ cleanUpOnCall = true, cleanUpOnDepsChange = false, cleanUpOnUnmount = true }: Options = {}
44114
) {
45-
const cleanupRef = useRef<Cleanup | null>(null);
115+
const cleanupRef = useRef<{ cleanup: Cleanup | null; isCalled: boolean }>({
116+
cleanup: null,
117+
isCalled: false,
118+
});
119+
120+
// re-construct options & keep stored in a ref to cheat through useEffect deps
121+
const options = { cleanUpOnCall, cleanUpOnDepsChange, cleanUpOnUnmount };
122+
const optionsRef = useRef(options);
123+
useEffect(() => {
124+
optionsRef.current = options;
125+
});
46126

47127
useEffect(() => {
48128
return () => {
49-
// clean up last call if applicable
50-
executeIfFunction(cleanupRef.current);
129+
if (optionsRef.current.cleanUpOnDepsChange) {
130+
handleCleanupObj(cleanupRef.current);
131+
}
132+
};
133+
}, [deps]);
134+
135+
useEffect(() => {
136+
return () => {
137+
if (optionsRef.current.cleanUpOnUnmount) {
138+
// clean up last call if applicable
139+
handleCleanupObj(cleanupRef.current);
140+
}
51141
};
52142
}, []);
53143

54144
const outputCallback = useCallback((...args: T) => {
55145
// clean up previous call if applicable
56-
executeIfFunction(cleanupRef.current);
146+
if (optionsRef.current.cleanUpOnCall) {
147+
handleCleanupObj(cleanupRef.current);
148+
}
57149

58150
const returnValue = callback(...args);
59151

60152
if (typeof returnValue === 'object') {
61153
const { value, cleanup } = returnValue;
62-
cleanupRef.current = cleanup;
154+
cleanupRef.current = {
155+
cleanup,
156+
isCalled: false,
157+
};
63158
return value;
64159
} else if (typeof returnValue === 'function') {
65-
cleanupRef.current = returnValue;
160+
cleanupRef.current = {
161+
cleanup: returnValue,
162+
isCalled: false,
163+
};
66164
}
67165

68166
return returnValue;

0 commit comments

Comments
 (0)