11import { useCallback , useEffect , DependencyList , useRef } from 'react' ;
22
3+ /**
4+ * A standard cleanup
5+ */
36export 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+ */
512export type ReturnObject < V > = {
613 value : V ;
714 cleanup : Cleanup ;
815} ;
916
17+ /**
18+ * An input/output callback that returns a standard cleanup
19+ */
1020export 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+ */
2978function 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+ */
3396function 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 */
37103function 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 */
41110function 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