Skip to content

Commit 1ba9973

Browse files
sean-perkinsthetaPCaveryjohnston
authored
fix(react): cleanup functions are execute for lifecycle hooks (#28319)
Issue number: Resolves #28186 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Ionic lifecycle hooks do not execute a cleanup function when the underlying `useEffect` is unmounted. ```ts useEffect(() => { return () => { console.log('cleanup'); // called }; }); useIonViewWillEnter(() => { return () => { console.log('cleanup'); // never called }; }); ``` Ionic's implementation registers the lifecycle callback to be handled at a later time, by the page managers. However, it does not keep a reference to the returned callback, so it cannot execute it when the `useEffect` is unmounted. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Ionic lifecycle hooks execute dev-specified cleanup functions ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev-build: `7.4.4-dev.11696956070.1faa3cfe` This PR builds on the changes in #28316. --------- Co-authored-by: Maria Hutt <[email protected]> Co-authored-by: Amanda Johnston <[email protected]>
1 parent dd93e0b commit 1ba9973

File tree

2 files changed

+113
-5
lines changed

2 files changed

+113
-5
lines changed

packages/react/src/contexts/IonLifeCycleContext.tsx

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface IonLifeCycleContextInterface {
99
ionViewWillLeave: () => void;
1010
onIonViewDidLeave: (callback: () => void) => void;
1111
ionViewDidLeave: () => void;
12+
cleanupIonViewWillEnter: (callback: () => void) => void;
13+
cleanupIonViewDidEnter: (callback: () => void) => void;
14+
cleanupIonViewWillLeave: (callback: () => void) => void;
15+
cleanupIonViewDidLeave: (callback: () => void) => void;
1216
}
1317

1418
export const IonLifeCycleContext = /*@__PURE__*/ React.createContext<IonLifeCycleContextInterface>({
@@ -36,19 +40,40 @@ export const IonLifeCycleContext = /*@__PURE__*/ React.createContext<IonLifeCycl
3640
ionViewDidLeave: () => {
3741
return;
3842
},
43+
cleanupIonViewWillEnter: () => {
44+
return;
45+
},
46+
cleanupIonViewDidEnter: () => {
47+
return;
48+
},
49+
cleanupIonViewWillLeave: () => {
50+
return;
51+
},
52+
cleanupIonViewDidLeave: () => {
53+
return;
54+
},
3955
});
4056

4157
export interface LifeCycleCallback {
42-
(): void;
58+
(): void | (() => void | undefined);
4359
id?: number;
4460
}
4561

62+
export interface LifeCycleDestructor {
63+
id: number;
64+
destructor: ReturnType<LifeCycleCallback>;
65+
}
66+
4667
export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextInterface {
4768
ionViewWillEnterCallbacks: LifeCycleCallback[] = [];
4869
ionViewDidEnterCallbacks: LifeCycleCallback[] = [];
4970
ionViewWillLeaveCallbacks: LifeCycleCallback[] = [];
5071
ionViewDidLeaveCallbacks: LifeCycleCallback[] = [];
5172
componentCanBeDestroyedCallback?: () => void;
73+
ionViewWillEnterDestructorCallbacks: LifeCycleDestructor[] = [];
74+
ionViewDidEnterDestructorCallbacks: LifeCycleDestructor[] = [];
75+
ionViewWillLeaveDestructorCallbacks: LifeCycleDestructor[] = [];
76+
ionViewDidLeaveDestructorCallbacks: LifeCycleDestructor[] = [];
5277

5378
onIonViewWillEnter(callback: LifeCycleCallback) {
5479
if (callback.id) {
@@ -63,8 +88,64 @@ export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextIn
6388
}
6489
}
6590

91+
teardownCallback(callback: LifeCycleCallback, callbacks: any[]) {
92+
// Find any destructors that have been registered for the callback
93+
const matches = callbacks.filter((x) => x.id === callback.id);
94+
if (matches.length !== 0) {
95+
// Execute the destructor for each matching item
96+
matches.forEach((match) => {
97+
if (match && typeof match.destructor === 'function') {
98+
match.destructor();
99+
}
100+
});
101+
// Remove all matching items from the array
102+
callbacks = callbacks.filter((x) => x.id !== callback.id);
103+
}
104+
}
105+
106+
/**
107+
* Tears down the user-provided ionViewWillEnter lifecycle callback.
108+
* This is the same behavior as React's useEffect hook. The callback
109+
* is invoked when the component is unmounted.
110+
*/
111+
cleanupIonViewWillEnter(callback: LifeCycleCallback) {
112+
this.teardownCallback(callback, this.ionViewWillEnterDestructorCallbacks);
113+
}
114+
115+
/**
116+
* Tears down the user-provided ionViewDidEnter lifecycle callback.
117+
* This is the same behavior as React's useEffect hook. The callback
118+
* is invoked when the component is unmounted.
119+
*/
120+
cleanupIonViewDidEnter(callback: LifeCycleCallback) {
121+
this.teardownCallback(callback, this.ionViewDidEnterDestructorCallbacks);
122+
}
123+
124+
/**
125+
* Tears down the user-provided ionViewWillLeave lifecycle callback.
126+
* This is the same behavior as React's useEffect hook. The callback
127+
* is invoked when the component is unmounted.
128+
*/
129+
cleanupIonViewWillLeave(callback: LifeCycleCallback) {
130+
this.teardownCallback(callback, this.ionViewWillLeaveDestructorCallbacks);
131+
}
132+
133+
/**
134+
* Tears down the user-provided ionViewDidLeave lifecycle callback.
135+
* This is the same behavior as React's useEffect hook. The callback
136+
* is invoked when the component is unmounted.
137+
*/
138+
cleanupIonViewDidLeave(callback: LifeCycleCallback) {
139+
this.teardownCallback(callback, this.ionViewDidLeaveDestructorCallbacks);
140+
}
141+
66142
ionViewWillEnter() {
67-
this.ionViewWillEnterCallbacks.forEach((cb) => cb());
143+
this.ionViewWillEnterCallbacks.forEach((cb) => {
144+
const destructor = cb();
145+
if (cb.id) {
146+
this.ionViewWillEnterDestructorCallbacks.push({ id: cb.id, destructor });
147+
}
148+
});
68149
}
69150

70151
onIonViewDidEnter(callback: LifeCycleCallback) {
@@ -81,7 +162,12 @@ export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextIn
81162
}
82163

83164
ionViewDidEnter() {
84-
this.ionViewDidEnterCallbacks.forEach((cb) => cb());
165+
this.ionViewDidEnterCallbacks.forEach((cb) => {
166+
const destructor = cb();
167+
if (cb.id) {
168+
this.ionViewDidEnterDestructorCallbacks.push({ id: cb.id, destructor });
169+
}
170+
});
85171
}
86172

87173
onIonViewWillLeave(callback: LifeCycleCallback) {
@@ -98,7 +184,12 @@ export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextIn
98184
}
99185

100186
ionViewWillLeave() {
101-
this.ionViewWillLeaveCallbacks.forEach((cb) => cb());
187+
this.ionViewWillLeaveCallbacks.forEach((cb) => {
188+
const destructor = cb();
189+
if (cb.id) {
190+
this.ionViewWillLeaveDestructorCallbacks.push({ id: cb.id, destructor });
191+
}
192+
});
102193
}
103194

104195
onIonViewDidLeave(callback: LifeCycleCallback) {
@@ -115,7 +206,12 @@ export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextIn
115206
}
116207

117208
ionViewDidLeave() {
118-
this.ionViewDidLeaveCallbacks.forEach((cb) => cb());
209+
this.ionViewDidLeaveCallbacks.forEach((cb) => {
210+
const destructor = cb();
211+
if (cb.id) {
212+
this.ionViewDidLeaveDestructorCallbacks.push({ id: cb.id, destructor });
213+
}
214+
});
119215
this.componentCanBeDestroyed();
120216
}
121217

packages/react/src/lifecycle/hooks.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export const useIonViewWillEnter = (callback: LifeCycleCallback, deps: any[] = [
1010
useEffect(() => {
1111
callback.id = id.current!;
1212
context.onIonViewWillEnter(callback);
13+
return () => {
14+
context.cleanupIonViewWillEnter(callback);
15+
};
1316
}, deps);
1417
};
1518

@@ -20,6 +23,9 @@ export const useIonViewDidEnter = (callback: LifeCycleCallback, deps: any[] = []
2023
useEffect(() => {
2124
callback.id = id.current!;
2225
context.onIonViewDidEnter(callback);
26+
return () => {
27+
context.cleanupIonViewDidEnter(callback);
28+
};
2329
}, deps);
2430
};
2531

@@ -30,6 +36,9 @@ export const useIonViewWillLeave = (callback: LifeCycleCallback, deps: any[] = [
3036
useEffect(() => {
3137
callback.id = id.current!;
3238
context.onIonViewWillLeave(callback);
39+
return () => {
40+
context.cleanupIonViewWillLeave(callback);
41+
};
3342
}, deps);
3443
};
3544

@@ -40,5 +49,8 @@ export const useIonViewDidLeave = (callback: LifeCycleCallback, deps: any[] = []
4049
useEffect(() => {
4150
callback.id = id.current!;
4251
context.onIonViewDidLeave(callback);
52+
return () => {
53+
context.cleanupIonViewDidLeave(callback);
54+
};
4355
}, deps);
4456
};

0 commit comments

Comments
 (0)