Skip to content

Commit 5cc2936

Browse files
authored
fix: Heap usage request throttling (#1450)
fixes #1439 - Heap usage code now guarantees that new requests won't be scheduled until all previous requests are complete. ## Testing I tested by shutting my Mac for ~2 hours and returning. Timings looks something like this: ### Summary <img width="310" alt="image" src="https://github.com/deephaven/web-client-ui/assets/1900643/0ad2d426-2fed-48e8-a255-989ee5d524f3"> It seems that requests still happen occasionally while sleeping. On Wake up, there were a few ticks resulting in "Unable to get heap usage" console errors. Then a prompt showed up indicating credentials had expired at which point requests start succeeding again. ### Full Console Log ``` useAsyncInterval.ts:37 [useAsyncInterval] tick #1. 10068 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9918 useAsyncInterval.ts:37 [useAsyncInterval] tick #2. 9936 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #3. 10026 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9958 useAsyncInterval.ts:62 [useAsyncInterval] Setting interval minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #1. 941977 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #2. 13 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #3. 2038465 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #4. 19 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #5. 537820 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #6. 16 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #7. 191633 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #8. 18 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #9. 23720 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #10. 18 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #11. 886367 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #12. 17 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #13. 1010951 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #14. 6 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #15. 650011 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #16. 13 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #17. 731687 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #18. 19 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #19. 120545 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #20. 16 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #21. 446345 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 0 useAsyncInterval.ts:37 [useAsyncInterval] tick #22. 13 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #23. 10999 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 8987 useAsyncInterval.ts:37 [useAsyncInterval] tick #24. 9020 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #25. 10030 ms elapsed since last tick. HeapUsage.tsx:68 [HeapUsage] Unable to get heap usage Error: Authentication details invalid useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9955 useAsyncInterval.ts:37 [useAsyncInterval] tick #26. 9979 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #27. 10027 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9956 useAsyncInterval.ts:62 [useAsyncInterval] Setting interval minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #1. 10695 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9290 useAsyncInterval.ts:37 [useAsyncInterval] tick #2. 9311 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 10000 useAsyncInterval.ts:37 [useAsyncInterval] tick #3. 10017 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9969 useAsyncInterval.ts:37 [useAsyncInterval] tick #4. 9992 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9992 useAsyncInterval.ts:37 [useAsyncInterval] tick #5. 10680 ms elapsed since last tick. useAsyncInterval.ts:56 [useAsyncInterval] adjusted minIntervalMs: 9308 ```
1 parent 938aa07 commit 5cc2936

9 files changed

Lines changed: 315 additions & 49 deletions

File tree

package-lock.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/console/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@deephaven/log": "file:../log",
3232
"@deephaven/storage": "file:../storage",
3333
"@deephaven/utils": "file:../utils",
34+
"@deephaven/react-hooks": "file:../react-hooks",
3435
"@fortawesome/react-fontawesome": "^0.2.0",
3536
"classnames": "^2.3.1",
3637
"linkifyjs": "^4.1.0",

packages/console/src/HeapUsage.tsx

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React, { useEffect, useState, ReactElement, useRef } from 'react';
1+
import { useState, ReactElement, useRef, useCallback } from 'react';
22
import classNames from 'classnames';
33
import { Tooltip } from '@deephaven/components';
44
import type { QueryConnectable } from '@deephaven/jsapi-types';
55
import { Plot, ChartTheme } from '@deephaven/chart';
66
import Log from '@deephaven/log';
7+
import { useAsyncInterval } from '@deephaven/react-hooks';
78
import './HeapUsage.scss';
89

910
const log = Log.module('HeapUsage');
@@ -38,55 +39,39 @@ function HeapUsage({
3839
usages: [],
3940
});
4041

41-
useEffect(
42-
function setUsageUpdateInterval() {
43-
const fetchAndUpdate = async () => {
44-
try {
45-
const newUsage = await connection.getWorkerHeapInfo();
46-
setMemoryUsage(newUsage);
47-
48-
if (bgMonitoring || isOpen) {
49-
const currentUsage =
50-
(newUsage.totalHeapSize - newUsage.freeMemory) /
51-
newUsage.maximumHeapSize;
52-
const currentTime = Date.now();
53-
54-
const { timestamps, usages } = historyUsage.current;
55-
while (
56-
timestamps.length !== 0 &&
57-
currentTime - timestamps[0] > monitorDuration * 1.5
58-
) {
59-
timestamps.shift();
60-
usages.shift();
61-
}
62-
63-
timestamps.push(currentTime);
64-
usages.push(currentUsage);
65-
} else {
66-
historyUsage.current = { timestamps: [], usages: [] };
67-
}
68-
} catch (e) {
69-
log.warn('Unable to get heap usage', e);
42+
const setUsageUpdateInterval = useCallback(async () => {
43+
try {
44+
const newUsage = await connection.getWorkerHeapInfo();
45+
setMemoryUsage(newUsage);
46+
47+
if (bgMonitoring || isOpen) {
48+
const currentUsage =
49+
(newUsage.totalHeapSize - newUsage.freeMemory) /
50+
newUsage.maximumHeapSize;
51+
const currentTime = Date.now();
52+
53+
const { timestamps, usages } = historyUsage.current;
54+
while (
55+
timestamps.length !== 0 &&
56+
currentTime - timestamps[0] > monitorDuration * 1.5
57+
) {
58+
timestamps.shift();
59+
usages.shift();
7060
}
71-
};
72-
fetchAndUpdate();
73-
74-
const updateUsage = setInterval(
75-
fetchAndUpdate,
76-
isOpen ? hoverUpdateInterval : defaultUpdateInterval
77-
);
78-
return () => {
79-
clearInterval(updateUsage);
80-
};
81-
},
82-
[
83-
isOpen,
84-
hoverUpdateInterval,
85-
connection,
86-
defaultUpdateInterval,
87-
monitorDuration,
88-
bgMonitoring,
89-
]
61+
62+
timestamps.push(currentTime);
63+
usages.push(currentUsage);
64+
} else {
65+
historyUsage.current = { timestamps: [], usages: [] };
66+
}
67+
} catch (e) {
68+
log.warn('Unable to get heap usage', e);
69+
}
70+
}, [isOpen, connection, monitorDuration, bgMonitoring]);
71+
72+
useAsyncInterval(
73+
setUsageUpdateInterval,
74+
isOpen ? hoverUpdateInterval : defaultUpdateInterval
9075
);
9176

9277
const toDecimalPlace = (num: number, dec: number) =>

packages/react-hooks/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
export * from './useAsyncInterval';
12
export { default as useContextOrThrow } from './useContextOrThrow';
23
export { default as usePrevious } from './usePrevious';
34
export { default as useForwardedRef } from './useForwardedRef';
45
export { default as useCopyToClipboard } from './useCopyToClipboard';
56
export { default as useFormWithDetachedSubmitButton } from './useFormWithDetachedSubmitButton';
7+
export * from './useIsMountedRef';
68
export { default as usePromiseFactory } from './usePromiseFactory';
79
export type { UseFormWithDetachedSubmitButtonResult } from './useFormWithDetachedSubmitButton';
810
export type {
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { renderHook, act } from '@testing-library/react-hooks';
2+
import { TestUtils } from '@deephaven/utils';
3+
import useAsyncInterval from './useAsyncInterval';
4+
5+
beforeEach(() => {
6+
jest.clearAllMocks();
7+
expect.hasAssertions();
8+
jest.useFakeTimers();
9+
jest.spyOn(window, 'setTimeout').mockName('setTimeout');
10+
});
11+
12+
afterAll(() => {
13+
jest.useRealTimers();
14+
});
15+
16+
describe('useAsyncInterval', () => {
17+
function createCallback(ms: number) {
18+
return jest.fn(
19+
async (): Promise<void> =>
20+
new Promise(resolve => {
21+
setTimeout(resolve, ms);
22+
})
23+
);
24+
}
25+
26+
const targetIntervalMs = 1000;
27+
28+
it('should call the callback function after the target interval', async () => {
29+
const callback = createCallback(50);
30+
31+
renderHook(() => useAsyncInterval(callback, targetIntervalMs));
32+
33+
// First tick should be scheduled for target interval
34+
expect(callback).not.toHaveBeenCalled();
35+
expect(window.setTimeout).toHaveBeenCalledWith(
36+
expect.any(Function),
37+
targetIntervalMs
38+
);
39+
40+
// Callback should be called after target interval
41+
act(() => jest.advanceTimersByTime(targetIntervalMs));
42+
expect(callback).toHaveBeenCalledTimes(1);
43+
});
44+
45+
it('should adjust the target interval based on how long async call takes', async () => {
46+
const callbackDelayMs = 50;
47+
const callback = createCallback(callbackDelayMs);
48+
49+
renderHook(() => useAsyncInterval(callback, targetIntervalMs));
50+
51+
// Callback should be called after target interval
52+
expect(callback).not.toHaveBeenCalled();
53+
act(() => jest.advanceTimersByTime(targetIntervalMs));
54+
expect(callback).toHaveBeenCalledTimes(1);
55+
56+
jest.clearAllMocks();
57+
58+
// Mimick the callback Promise resolving
59+
act(() => jest.advanceTimersByTime(callbackDelayMs));
60+
await TestUtils.flushPromises();
61+
62+
// Next target interval should be adjusted based on how long the callback took
63+
const nextTargetIntervalMs = targetIntervalMs - callbackDelayMs;
64+
65+
expect(callback).not.toHaveBeenCalled();
66+
expect(window.setTimeout).toHaveBeenCalledTimes(1);
67+
expect(window.setTimeout).toHaveBeenCalledWith(
68+
expect.any(Function),
69+
nextTargetIntervalMs
70+
);
71+
72+
act(() => jest.advanceTimersByTime(nextTargetIntervalMs));
73+
expect(callback).toHaveBeenCalledTimes(1);
74+
});
75+
76+
it('should schedule the next callback immediately if the callback takes longer than the target interval', async () => {
77+
const callbackDelayMs = targetIntervalMs + 50;
78+
const callback = createCallback(callbackDelayMs);
79+
80+
renderHook(() => useAsyncInterval(callback, targetIntervalMs));
81+
82+
// Callback should be called after target interval
83+
expect(callback).not.toHaveBeenCalled();
84+
act(() => jest.advanceTimersByTime(targetIntervalMs));
85+
expect(callback).toHaveBeenCalledTimes(1);
86+
87+
jest.clearAllMocks();
88+
89+
// Mimick the callback Promise resolving
90+
act(() => jest.advanceTimersByTime(callbackDelayMs));
91+
await TestUtils.flushPromises();
92+
93+
expect(callback).not.toHaveBeenCalled();
94+
expect(window.setTimeout).toHaveBeenCalledTimes(1);
95+
expect(window.setTimeout).toHaveBeenCalledWith(expect.any(Function), 0);
96+
97+
act(() => jest.advanceTimersByTime(0));
98+
expect(callback).toHaveBeenCalledTimes(1);
99+
});
100+
101+
it('should stop calling the callback function after unmounting', async () => {
102+
const callback = createCallback(50);
103+
104+
const { unmount } = renderHook(() =>
105+
useAsyncInterval(callback, targetIntervalMs)
106+
);
107+
108+
unmount();
109+
110+
act(() => jest.advanceTimersByTime(targetIntervalMs));
111+
112+
expect(callback).not.toHaveBeenCalled();
113+
});
114+
115+
it('should not re-schedule callback if callback resolves after unmounting', async () => {
116+
const callbackDelayMs = 50;
117+
const callback = createCallback(callbackDelayMs);
118+
119+
const { unmount } = renderHook(() =>
120+
useAsyncInterval(callback, targetIntervalMs)
121+
);
122+
123+
act(() => jest.advanceTimersByTime(targetIntervalMs));
124+
expect(callback).toHaveBeenCalledTimes(1);
125+
jest.clearAllMocks();
126+
127+
unmount();
128+
129+
// Mimick the callback Promise resolving
130+
act(() => jest.advanceTimersByTime(callbackDelayMs));
131+
await TestUtils.flushPromises();
132+
133+
expect(window.setTimeout).not.toHaveBeenCalled();
134+
});
135+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useCallback, useEffect, useRef } from 'react';
2+
import Log from '@deephaven/log';
3+
import { useIsMountedRef } from './useIsMountedRef';
4+
5+
const log = Log.module('useAsyncInterval');
6+
7+
/**
8+
* Calls the given async callback at a target interval.
9+
*
10+
* If the callback takes less time than the target interval, the target interval
11+
* for the next tick will be adjusted to target the remaining time in the current
12+
* interval.
13+
*
14+
* e.g. If the target interval is 1000ms, and the callback takes 50ms to resolve,
15+
* the next tick will be scheduled for 950ms from now via `setTimeout(callback, 950)`.
16+
*
17+
* If the callback takes longer than the target interval, the next tick will be
18+
* scheduled immediately via `setTimeout(callback, 0)`. In such cases, the time
19+
* between ticks may be > than the target interval, but this guarantees that
20+
* a callback won't be scheduled until after the previous one has resolved.
21+
* @param callback Callback to call at the target interval
22+
* @param targetIntervalMs Target interval in milliseconds to call the callback
23+
*/
24+
export function useAsyncInterval(
25+
callback: () => Promise<void>,
26+
targetIntervalMs: number
27+
) {
28+
const isMountedRef = useIsMountedRef();
29+
const trackingRef = useRef({ count: 0, started: Date.now() });
30+
const setTimeoutRef = useRef(0);
31+
32+
const tick = useCallback(async () => {
33+
const now = Date.now();
34+
let elapsedSinceLastTick = now - trackingRef.current.started;
35+
36+
trackingRef.current.count += 1;
37+
trackingRef.current.started = now;
38+
39+
log.debug(
40+
`tick #${trackingRef.current.count}.`,
41+
elapsedSinceLastTick,
42+
'ms elapsed since last tick.'
43+
);
44+
45+
await callback();
46+
47+
if (!isMountedRef.current) {
48+
return;
49+
}
50+
51+
elapsedSinceLastTick += Date.now() - trackingRef.current.started;
52+
53+
// If elapsed time is > than the target interval, adjust the next tick interval
54+
const nextTickInterval = Math.max(
55+
0,
56+
Math.min(
57+
targetIntervalMs,
58+
targetIntervalMs - (elapsedSinceLastTick - targetIntervalMs)
59+
)
60+
);
61+
62+
log.debug('adjusted minIntervalMs:', nextTickInterval);
63+
64+
setTimeoutRef.current = window.setTimeout(tick, nextTickInterval);
65+
}, [callback, isMountedRef, targetIntervalMs]);
66+
67+
useEffect(() => {
68+
log.debug('Setting interval minIntervalMs:', targetIntervalMs);
69+
70+
setTimeoutRef.current = window.setTimeout(tick, targetIntervalMs);
71+
72+
return () => {
73+
window.clearTimeout(setTimeoutRef.current);
74+
};
75+
}, [targetIntervalMs, tick]);
76+
}
77+
78+
export default useAsyncInterval;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import { useIsMountedRef } from './useIsMountedRef';
3+
4+
beforeEach(() => {
5+
jest.clearAllMocks();
6+
expect.hasAssertions();
7+
});
8+
9+
describe('useIsMountedRef', () => {
10+
it('should return a ref which tracks whether the component is mounted or not', () => {
11+
const { result, unmount } = renderHook(() => useIsMountedRef());
12+
13+
expect(result.current.current).toBe(true);
14+
15+
unmount();
16+
17+
expect(result.current.current).toBe(false);
18+
});
19+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
/**
4+
* Returns a ref which tracks whether the component is mounted or not.
5+
*/
6+
export function useIsMountedRef() {
7+
const isMountedRef = useRef(false);
8+
9+
useEffect(() => {
10+
isMountedRef.current = true;
11+
12+
return () => {
13+
isMountedRef.current = false;
14+
};
15+
}, []);
16+
17+
return isMountedRef;
18+
}
19+
20+
export default useIsMountedRef;

0 commit comments

Comments
 (0)