@umituz/web-design-system 1.0.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/package.json +18 -5
- package/src/global.d.ts +20 -0
- package/src/index.ts +3 -0
- package/src/infrastructure/error/ErrorBoundary.tsx +258 -0
- package/src/infrastructure/error/ErrorDisplay.tsx +346 -0
- package/src/infrastructure/error/SuspenseWrapper.tsx +134 -0
- package/src/infrastructure/error/index.ts +5 -0
- package/src/infrastructure/performance/index.ts +6 -0
- package/src/infrastructure/performance/useLazyLoading.ts +342 -0
- package/src/infrastructure/performance/useMemoryOptimization.ts +293 -0
- package/src/infrastructure/performance/usePerformanceMonitor.ts +158 -0
- package/src/infrastructure/security/index.ts +10 -0
- package/src/infrastructure/security/security-config.ts +171 -0
- package/src/infrastructure/security/useFormValidation.ts +216 -0
- package/src/infrastructure/security/validation.ts +242 -0
- package/src/infrastructure/utils/cn.util.ts +7 -7
- package/src/infrastructure/utils/index.ts +1 -1
- package/src/presentation/atoms/Input.tsx +1 -1
- package/src/presentation/atoms/Slider.tsx +1 -1
- package/src/presentation/atoms/Text.tsx +1 -1
- package/src/presentation/atoms/Tooltip.tsx +2 -2
- package/src/presentation/hooks/index.ts +2 -1
- package/src/presentation/hooks/useClickOutside.ts +1 -1
- package/src/presentation/molecules/CheckboxGroup.tsx +1 -1
- package/src/presentation/molecules/FormField.tsx +2 -2
- package/src/presentation/molecules/InputGroup.tsx +1 -1
- package/src/presentation/molecules/RadioGroup.tsx +1 -1
- package/src/presentation/organisms/Alert.tsx +1 -1
- package/src/presentation/organisms/Card.tsx +1 -1
- package/src/presentation/organisms/Footer.tsx +99 -0
- package/src/presentation/organisms/Navbar.tsx +1 -1
- package/src/presentation/organisms/Table.tsx +1 -1
- package/src/presentation/organisms/index.ts +3 -0
- package/src/presentation/templates/Form.tsx +2 -2
- package/src/presentation/templates/List.tsx +1 -1
- package/src/presentation/templates/PageHeader.tsx +78 -0
- package/src/presentation/templates/PageLayout.tsx +38 -0
- package/src/presentation/templates/ProjectSkeleton.tsx +35 -0
- package/src/presentation/templates/Section.tsx +1 -1
- package/src/presentation/templates/index.ts +9 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useIntersectionObserver = (
|
|
4
|
+
options: IntersectionObserverInit = {}
|
|
5
|
+
) => {
|
|
6
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
7
|
+
const [hasIntersected, setHasIntersected] = useState(false);
|
|
8
|
+
const targetRef = useRef<HTMLElement>(null);
|
|
9
|
+
|
|
10
|
+
const defaultOptions = React.useMemo(() => ({
|
|
11
|
+
threshold: 0.1,
|
|
12
|
+
rootMargin: '50px',
|
|
13
|
+
...options,
|
|
14
|
+
}), [options]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const target = targetRef.current;
|
|
18
|
+
if (!target) return;
|
|
19
|
+
|
|
20
|
+
const observer = new IntersectionObserver(
|
|
21
|
+
([entry]) => {
|
|
22
|
+
const isVisible = entry.isIntersecting;
|
|
23
|
+
setIsIntersecting(isVisible);
|
|
24
|
+
|
|
25
|
+
if (isVisible && !hasIntersected) {
|
|
26
|
+
setHasIntersected(true);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
defaultOptions
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
observer.observe(target);
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
observer.unobserve(target);
|
|
36
|
+
};
|
|
37
|
+
}, [defaultOptions, hasIntersected]);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
targetRef,
|
|
41
|
+
isIntersecting,
|
|
42
|
+
hasIntersected,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const useLazyImage = (src: string, placeholder?: string) => {
|
|
47
|
+
const [imageSrc, setImageSrc] = useState(placeholder || '');
|
|
48
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
49
|
+
const [isError, setIsError] = useState(false);
|
|
50
|
+
const { targetRef, hasIntersected } = useIntersectionObserver();
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!hasIntersected || !src) return;
|
|
54
|
+
|
|
55
|
+
const img = new Image();
|
|
56
|
+
|
|
57
|
+
img.onload = () => {
|
|
58
|
+
setImageSrc(src);
|
|
59
|
+
setIsLoaded(true);
|
|
60
|
+
setIsError(false);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
img.onerror = () => {
|
|
64
|
+
setIsError(true);
|
|
65
|
+
setIsLoaded(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
img.src = src;
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
img.onload = null;
|
|
72
|
+
img.onerror = null;
|
|
73
|
+
};
|
|
74
|
+
}, [hasIntersected, src]);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
targetRef,
|
|
78
|
+
imageSrc,
|
|
79
|
+
isLoaded,
|
|
80
|
+
isError,
|
|
81
|
+
hasIntersected,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const useVirtualList = <T>(
|
|
86
|
+
items: T[],
|
|
87
|
+
options: {
|
|
88
|
+
itemHeight: number;
|
|
89
|
+
containerHeight: number;
|
|
90
|
+
overscan?: number;
|
|
91
|
+
}
|
|
92
|
+
) => {
|
|
93
|
+
const { itemHeight, containerHeight, overscan = 5 } = options;
|
|
94
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
95
|
+
const scrollElementRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
|
|
97
|
+
const totalHeight = items.length * itemHeight;
|
|
98
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
|
|
99
|
+
const endIndex = Math.min(
|
|
100
|
+
items.length - 1,
|
|
101
|
+
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const visibleItems = items.slice(startIndex, endIndex + 1).map((item, index) => ({
|
|
105
|
+
item,
|
|
106
|
+
index: startIndex + index,
|
|
107
|
+
style: {
|
|
108
|
+
position: 'absolute' as const,
|
|
109
|
+
top: (startIndex + index) * itemHeight,
|
|
110
|
+
height: itemHeight,
|
|
111
|
+
width: '100%',
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
|
116
|
+
setScrollTop(event.currentTarget.scrollTop);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
scrollElementRef,
|
|
121
|
+
visibleItems,
|
|
122
|
+
totalHeight,
|
|
123
|
+
handleScroll,
|
|
124
|
+
startIndex,
|
|
125
|
+
endIndex,
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const useResourcePreloader = () => {
|
|
130
|
+
const [preloadedResources, setPreloadedResources] = useState<Set<string>>(new Set());
|
|
131
|
+
const [loadingResources, setLoadingResources] = useState<Set<string>>(new Set());
|
|
132
|
+
|
|
133
|
+
const preloadImage = useCallback(async (src: string): Promise<void> => {
|
|
134
|
+
if (preloadedResources.has(src) || loadingResources.has(src)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setLoadingResources(prev => new Set(prev).add(src));
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const img = new Image();
|
|
142
|
+
|
|
143
|
+
img.onload = () => {
|
|
144
|
+
setPreloadedResources(prev => new Set(prev).add(src));
|
|
145
|
+
setLoadingResources(prev => {
|
|
146
|
+
const newSet = new Set(prev);
|
|
147
|
+
newSet.delete(src);
|
|
148
|
+
return newSet;
|
|
149
|
+
});
|
|
150
|
+
resolve();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
img.onerror = () => {
|
|
154
|
+
setLoadingResources(prev => {
|
|
155
|
+
const newSet = new Set(prev);
|
|
156
|
+
newSet.delete(src);
|
|
157
|
+
return newSet;
|
|
158
|
+
});
|
|
159
|
+
reject(new Error(`Failed to preload image: ${src}`));
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
img.src = src;
|
|
163
|
+
});
|
|
164
|
+
}, [preloadedResources, loadingResources]);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
preloadImage,
|
|
168
|
+
preloadedResources: Array.from(preloadedResources),
|
|
169
|
+
loadingResources: Array.from(loadingResources),
|
|
170
|
+
isPreloaded: (src: string) => preloadedResources.has(src),
|
|
171
|
+
isLoading: (src: string) => loadingResources.has(src),
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const useLazyComponent = <TProps = Record<string, unknown>>(
|
|
176
|
+
importFunc: () => Promise<{ default: React.ComponentType<TProps> }>,
|
|
177
|
+
fallback?: React.ComponentType<TProps>
|
|
178
|
+
) => {
|
|
179
|
+
const [Component, setComponent] = useState<React.ComponentType<TProps> | null>(null);
|
|
180
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
181
|
+
const [error, setError] = useState<Error | null>(null);
|
|
182
|
+
const { targetRef, hasIntersected } = useIntersectionObserver();
|
|
183
|
+
|
|
184
|
+
const loadComponent = useCallback(async () => {
|
|
185
|
+
if (Component || isLoading) return;
|
|
186
|
+
|
|
187
|
+
setIsLoading(true);
|
|
188
|
+
setError(null);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const module = await importFunc();
|
|
192
|
+
setComponent(() => module.default);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
setError(err instanceof Error ? err : new Error('Failed to load component'));
|
|
195
|
+
} finally {
|
|
196
|
+
setIsLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}, [importFunc, Component, isLoading]);
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (hasIntersected) {
|
|
202
|
+
loadComponent();
|
|
203
|
+
}
|
|
204
|
+
}, [hasIntersected, loadComponent]);
|
|
205
|
+
|
|
206
|
+
const LazyComponent = useCallback((props: TProps) => {
|
|
207
|
+
if (error) {
|
|
208
|
+
return fallback ? React.createElement(fallback as React.ElementType, props) : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (isLoading || !Component) {
|
|
212
|
+
return fallback ? React.createElement(fallback as React.ElementType, props) : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return React.createElement(Component as React.ElementType, props);
|
|
216
|
+
}, [Component, isLoading, error, fallback]);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
targetRef,
|
|
220
|
+
LazyComponent,
|
|
221
|
+
isLoading,
|
|
222
|
+
error,
|
|
223
|
+
hasIntersected,
|
|
224
|
+
loadComponent,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const useProgressiveEnhancement = () => {
|
|
229
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
230
|
+
const [connectionType, setConnectionType] = useState<'slow' | 'fast' | 'unknown'>('unknown');
|
|
231
|
+
|
|
232
|
+
const handleOnline = useCallback(() => {
|
|
233
|
+
setIsOnline(true);
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
const handleOffline = useCallback(() => {
|
|
237
|
+
setIsOnline(false);
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
window.addEventListener('online', handleOnline);
|
|
242
|
+
window.addEventListener('offline', handleOffline);
|
|
243
|
+
|
|
244
|
+
interface NetworkInformation {
|
|
245
|
+
effectiveType?: string;
|
|
246
|
+
addEventListener?: (type: string, listener: () => void) => void;
|
|
247
|
+
removeEventListener?: (type: string, listener: () => void) => void;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getConnection(): NetworkInformation | undefined {
|
|
251
|
+
const nav = navigator as unknown as {
|
|
252
|
+
connection?: unknown;
|
|
253
|
+
mozConnection?: unknown;
|
|
254
|
+
webkitConnection?: unknown;
|
|
255
|
+
};
|
|
256
|
+
const conn = nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
257
|
+
if (
|
|
258
|
+
conn &&
|
|
259
|
+
(typeof conn === 'object') &&
|
|
260
|
+
('effectiveType' in conn)
|
|
261
|
+
) {
|
|
262
|
+
return conn as NetworkInformation;
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const connection = getConnection();
|
|
268
|
+
|
|
269
|
+
if (connection) {
|
|
270
|
+
const updateConnectionType = () => {
|
|
271
|
+
const effectiveType = connection.effectiveType;
|
|
272
|
+
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
|
|
273
|
+
setConnectionType('slow');
|
|
274
|
+
} else if (effectiveType === '3g' || effectiveType === '4g') {
|
|
275
|
+
setConnectionType('fast');
|
|
276
|
+
} else {
|
|
277
|
+
setConnectionType('unknown');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
updateConnectionType();
|
|
282
|
+
connection.addEventListener?.('change', updateConnectionType);
|
|
283
|
+
|
|
284
|
+
return () => {
|
|
285
|
+
window.removeEventListener('online', handleOnline);
|
|
286
|
+
window.removeEventListener('offline', handleOffline);
|
|
287
|
+
connection.removeEventListener?.('change', updateConnectionType);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return () => {
|
|
292
|
+
window.removeEventListener('online', handleOnline);
|
|
293
|
+
window.removeEventListener('offline', handleOffline);
|
|
294
|
+
};
|
|
295
|
+
}, [handleOnline, handleOffline]);
|
|
296
|
+
|
|
297
|
+
const shouldLoadHeavyContent = useCallback(() => {
|
|
298
|
+
return isOnline && connectionType !== 'slow';
|
|
299
|
+
}, [isOnline, connectionType]);
|
|
300
|
+
|
|
301
|
+
const shouldPreloadContent = useCallback(() => {
|
|
302
|
+
return isOnline && connectionType === 'fast';
|
|
303
|
+
}, [isOnline, connectionType]);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
isOnline,
|
|
307
|
+
connectionType,
|
|
308
|
+
shouldLoadHeavyContent,
|
|
309
|
+
shouldPreloadContent,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const useLazyLoading = (options: {
|
|
314
|
+
enableImageLazyLoading?: boolean;
|
|
315
|
+
enableComponentLazyLoading?: boolean;
|
|
316
|
+
enableVirtualization?: boolean;
|
|
317
|
+
enablePreloading?: boolean;
|
|
318
|
+
enableProgressiveEnhancement?: boolean;
|
|
319
|
+
} = {}) => {
|
|
320
|
+
const {
|
|
321
|
+
enableImageLazyLoading = true,
|
|
322
|
+
enableComponentLazyLoading = true,
|
|
323
|
+
enableVirtualization = true,
|
|
324
|
+
enablePreloading = true,
|
|
325
|
+
enableProgressiveEnhancement = true,
|
|
326
|
+
} = options;
|
|
327
|
+
|
|
328
|
+
const resourcePreloader = useResourcePreloader();
|
|
329
|
+
const progressiveEnhancement = useProgressiveEnhancement();
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
useIntersectionObserver,
|
|
333
|
+
useLazyImage: enableImageLazyLoading ? useLazyImage : null,
|
|
334
|
+
useLazyComponent: enableComponentLazyLoading ? useLazyComponent : null,
|
|
335
|
+
useVirtualList: enableVirtualization ? useVirtualList : null,
|
|
336
|
+
preloadImage: enablePreloading ? resourcePreloader.preloadImage : undefined,
|
|
337
|
+
shouldLoadHeavyContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldLoadHeavyContent : undefined,
|
|
338
|
+
shouldPreloadContent: enableProgressiveEnhancement ? progressiveEnhancement.shouldPreloadContent : undefined,
|
|
339
|
+
isOnline: enableProgressiveEnhancement ? progressiveEnhancement.isOnline : undefined,
|
|
340
|
+
connectionType: enableProgressiveEnhancement ? progressiveEnhancement.connectionType : undefined,
|
|
341
|
+
};
|
|
342
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface MemoryOptimizationConfig {
|
|
4
|
+
enableCleanupLogging?: boolean;
|
|
5
|
+
trackEventListeners?: boolean;
|
|
6
|
+
trackTimers?: boolean;
|
|
7
|
+
trackSubscriptions?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CleanupFunction {
|
|
11
|
+
(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useMemoryOptimization = (config: MemoryOptimizationConfig = {}) => {
|
|
15
|
+
const {
|
|
16
|
+
enableCleanupLogging = import.meta.env.DEV,
|
|
17
|
+
trackEventListeners = true,
|
|
18
|
+
trackTimers = true,
|
|
19
|
+
trackSubscriptions = true
|
|
20
|
+
} = config;
|
|
21
|
+
|
|
22
|
+
const cleanupFunctions = useRef<CleanupFunction[]>([]);
|
|
23
|
+
const eventListeners = useRef<Array<{
|
|
24
|
+
element: EventTarget;
|
|
25
|
+
event: string;
|
|
26
|
+
handler: EventListener;
|
|
27
|
+
options?: AddEventListenerOptions;
|
|
28
|
+
}>>([]);
|
|
29
|
+
const timers = useRef<Array<{
|
|
30
|
+
id: number;
|
|
31
|
+
type: 'timeout' | 'interval';
|
|
32
|
+
}>>([]);
|
|
33
|
+
const subscriptions = useRef<Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
unsubscribe: () => void;
|
|
36
|
+
}>>([]);
|
|
37
|
+
|
|
38
|
+
const addCleanup = useCallback((cleanup: CleanupFunction, name?: string) => {
|
|
39
|
+
cleanupFunctions.current.push(cleanup);
|
|
40
|
+
|
|
41
|
+
if (enableCleanupLogging && name) {
|
|
42
|
+
console.log(`[Memory] Added cleanup: ${name}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return () => {
|
|
46
|
+
const index = cleanupFunctions.current.indexOf(cleanup);
|
|
47
|
+
if (index > -1) {
|
|
48
|
+
cleanupFunctions.current.splice(index, 1);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}, [enableCleanupLogging]);
|
|
52
|
+
|
|
53
|
+
const addEventListener = useCallback((
|
|
54
|
+
element: EventTarget,
|
|
55
|
+
event: string,
|
|
56
|
+
handler: EventListener,
|
|
57
|
+
options?: AddEventListenerOptions
|
|
58
|
+
) => {
|
|
59
|
+
if (!trackEventListeners) {
|
|
60
|
+
element.addEventListener(event, handler, options);
|
|
61
|
+
return () => element.removeEventListener(event, handler, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
element.addEventListener(event, handler, options);
|
|
65
|
+
|
|
66
|
+
const listenerInfo = { element, event, handler, options };
|
|
67
|
+
eventListeners.current.push(listenerInfo);
|
|
68
|
+
|
|
69
|
+
if (enableCleanupLogging) {
|
|
70
|
+
console.log(`[Memory] Added event listener: ${event}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
element.removeEventListener(event, handler, options);
|
|
75
|
+
const index = eventListeners.current.indexOf(listenerInfo);
|
|
76
|
+
if (index > -1) {
|
|
77
|
+
eventListeners.current.splice(index, 1);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}, [trackEventListeners, enableCleanupLogging]);
|
|
81
|
+
|
|
82
|
+
const setTimeout = useCallback((
|
|
83
|
+
callback: () => void,
|
|
84
|
+
delay: number
|
|
85
|
+
) => {
|
|
86
|
+
const id = window.setTimeout(callback, delay);
|
|
87
|
+
|
|
88
|
+
if (trackTimers) {
|
|
89
|
+
timers.current.push({ id, type: 'timeout' });
|
|
90
|
+
|
|
91
|
+
if (enableCleanupLogging) {
|
|
92
|
+
console.log(`[Memory] Added timeout: ${id}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return id;
|
|
97
|
+
}, [trackTimers, enableCleanupLogging]);
|
|
98
|
+
|
|
99
|
+
const setInterval = useCallback((
|
|
100
|
+
callback: () => void,
|
|
101
|
+
delay: number
|
|
102
|
+
) => {
|
|
103
|
+
const id = window.setInterval(callback, delay);
|
|
104
|
+
|
|
105
|
+
if (trackTimers) {
|
|
106
|
+
timers.current.push({ id, type: 'interval' });
|
|
107
|
+
|
|
108
|
+
if (enableCleanupLogging) {
|
|
109
|
+
console.log(`[Memory] Added interval: ${id}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return id;
|
|
114
|
+
}, [trackTimers, enableCleanupLogging]);
|
|
115
|
+
|
|
116
|
+
const clearTimeout = useCallback((id: number) => {
|
|
117
|
+
window.clearTimeout(id);
|
|
118
|
+
|
|
119
|
+
if (trackTimers) {
|
|
120
|
+
const index = timers.current.findIndex(timer => timer.id === id);
|
|
121
|
+
if (index > -1) {
|
|
122
|
+
timers.current.splice(index, 1);
|
|
123
|
+
|
|
124
|
+
if (enableCleanupLogging) {
|
|
125
|
+
console.log(`[Memory] Cleared timeout: ${id}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, [trackTimers, enableCleanupLogging]);
|
|
130
|
+
|
|
131
|
+
const clearInterval = useCallback((id: number) => {
|
|
132
|
+
window.clearInterval(id);
|
|
133
|
+
|
|
134
|
+
if (trackTimers) {
|
|
135
|
+
const index = timers.current.findIndex(timer => timer.id === id);
|
|
136
|
+
if (index > -1) {
|
|
137
|
+
timers.current.splice(index, 1);
|
|
138
|
+
|
|
139
|
+
if (enableCleanupLogging) {
|
|
140
|
+
console.log(`[Memory] Cleared interval: ${id}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}, [trackTimers, enableCleanupLogging]);
|
|
145
|
+
|
|
146
|
+
const addSubscription = useCallback((
|
|
147
|
+
name: string,
|
|
148
|
+
unsubscribe: () => void
|
|
149
|
+
) => {
|
|
150
|
+
if (!trackSubscriptions) {
|
|
151
|
+
return unsubscribe;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const subscription = { name, unsubscribe };
|
|
155
|
+
subscriptions.current.push(subscription);
|
|
156
|
+
|
|
157
|
+
if (enableCleanupLogging) {
|
|
158
|
+
console.log(`[Memory] Added subscription: ${name}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
unsubscribe();
|
|
163
|
+
const index = subscriptions.current.indexOf(subscription);
|
|
164
|
+
if (index > -1) {
|
|
165
|
+
subscriptions.current.splice(index, 1);
|
|
166
|
+
|
|
167
|
+
if (enableCleanupLogging) {
|
|
168
|
+
console.log(`[Memory] Unsubscribed: ${name}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}, [trackSubscriptions, enableCleanupLogging]);
|
|
173
|
+
|
|
174
|
+
const getMemoryStats = useCallback(() => {
|
|
175
|
+
return {
|
|
176
|
+
cleanupFunctions: cleanupFunctions.current.length,
|
|
177
|
+
eventListeners: eventListeners.current.length,
|
|
178
|
+
timers: timers.current.length,
|
|
179
|
+
subscriptions: subscriptions.current.length,
|
|
180
|
+
totalTrackedItems:
|
|
181
|
+
cleanupFunctions.current.length +
|
|
182
|
+
eventListeners.current.length +
|
|
183
|
+
timers.current.length +
|
|
184
|
+
subscriptions.current.length
|
|
185
|
+
};
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
const cleanup = useCallback(() => {
|
|
189
|
+
if (enableCleanupLogging) {
|
|
190
|
+
const stats = getMemoryStats();
|
|
191
|
+
console.log(`[Memory] Running cleanup with ${stats.totalTrackedItems} items`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
eventListeners.current.forEach(({ element, event, handler, options }) => {
|
|
195
|
+
element.removeEventListener(event, handler, options);
|
|
196
|
+
});
|
|
197
|
+
eventListeners.current = [];
|
|
198
|
+
|
|
199
|
+
timers.current.forEach(({ id, type }) => {
|
|
200
|
+
if (type === 'timeout') {
|
|
201
|
+
window.clearTimeout(id);
|
|
202
|
+
} else {
|
|
203
|
+
window.clearInterval(id);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
timers.current = [];
|
|
207
|
+
|
|
208
|
+
subscriptions.current.forEach(({ unsubscribe }) => {
|
|
209
|
+
unsubscribe();
|
|
210
|
+
});
|
|
211
|
+
subscriptions.current = [];
|
|
212
|
+
|
|
213
|
+
cleanupFunctions.current.forEach(cleanup => {
|
|
214
|
+
try {
|
|
215
|
+
cleanup();
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('[Memory] Cleanup function failed:', error);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
cleanupFunctions.current = [];
|
|
221
|
+
|
|
222
|
+
if (enableCleanupLogging) {
|
|
223
|
+
console.log('[Memory] Cleanup complete');
|
|
224
|
+
}
|
|
225
|
+
}, [enableCleanupLogging, getMemoryStats]);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
return () => {
|
|
229
|
+
if (enableCleanupLogging) {
|
|
230
|
+
const stats = getMemoryStats();
|
|
231
|
+
console.log(`[Memory] Unmounting with ${stats.totalTrackedItems} items to clean`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
cleanup();
|
|
235
|
+
};
|
|
236
|
+
}, [cleanup, enableCleanupLogging, getMemoryStats]);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
addCleanup,
|
|
240
|
+
addEventListener,
|
|
241
|
+
setTimeout,
|
|
242
|
+
setInterval,
|
|
243
|
+
clearTimeout,
|
|
244
|
+
clearInterval,
|
|
245
|
+
addSubscription,
|
|
246
|
+
getMemoryStats,
|
|
247
|
+
cleanup
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const useMemoryLeakDetector = (componentName?: string) => {
|
|
252
|
+
const mountTime = useRef<number>(Date.now());
|
|
253
|
+
const [renderCount, setRenderCount] = useState<number>(0);
|
|
254
|
+
const [lifespan, setLifespan] = useState<number>(0);
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
setRenderCount(prev => {
|
|
258
|
+
const newCount = prev + 1;
|
|
259
|
+
|
|
260
|
+
if (newCount > 100 && newCount % 10 === 0) {
|
|
261
|
+
console.warn(
|
|
262
|
+
`[Memory Leak] ${componentName || 'Component'} has rendered ${newCount} times. ` +
|
|
263
|
+
'This might indicate a memory leak or unnecessary re-renders.'
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return newCount;
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
const interval = setInterval(() => {
|
|
273
|
+
setLifespan(Date.now() - mountTime.current);
|
|
274
|
+
}, 1000);
|
|
275
|
+
|
|
276
|
+
return () => {
|
|
277
|
+
clearInterval(interval);
|
|
278
|
+
const finalLifespan = Date.now() - mountTime.current;
|
|
279
|
+
|
|
280
|
+
if (import.meta.env.DEV) {
|
|
281
|
+
console.log(
|
|
282
|
+
`[Memory] ${componentName || 'Component'} unmounted after ${finalLifespan}ms ` +
|
|
283
|
+
`with ${renderCount} renders`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}, [componentName, renderCount]);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
renderCount,
|
|
291
|
+
lifespan
|
|
292
|
+
};
|
|
293
|
+
};
|