Skip to content

Commit c90a4b4

Browse files
committed
feat: New useLazy hook
1 parent 0638606 commit c90a4b4

File tree

6 files changed

+288
-1
lines changed

6 files changed

+288
-1
lines changed

packages/hooks/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @workspace/hooks
22

3+
## 1.1.0
4+
5+
### Minor Changes
6+
7+
- New useLazy hook
8+
39
## 1.0.0
410

511
### Major Changes

packages/hooks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@workspace/hooks",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"type": "module",
55
"private": true,
66
"exports": {

packages/hooks/src/index.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,20 @@
309309
}
310310
]
311311
},
312+
{
313+
"name": "use-lazy",
314+
"title": "useLazy",
315+
"description": "A hook for managing lazy-loaded React components with loading states, error handling, and preloading capabilities",
316+
"category": "performance",
317+
"dependencies": [],
318+
"devDependencies": [],
319+
"files": [
320+
{
321+
"name": "index.ts",
322+
"type": "hook"
323+
}
324+
]
325+
},
312326
{
313327
"name": "use-local-storage",
314328
"title": "useLocalStorage",

packages/hooks/src/use-lazy/doc.json

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"name": "useLazy",
3+
"description": "A powerful React hook that enhances React.lazy with loading states, error handling, preloading capabilities, and manual control over component loading. Perfect for code splitting and performance optimization.",
4+
"category": "performance",
5+
"version": "1.0.0",
6+
"parameters": [
7+
{
8+
"name": "importFn",
9+
"type": "() => Promise<{ default: T } | T>",
10+
"optional": false,
11+
"description": "Function that returns a promise resolving to the component to be lazy loaded"
12+
},
13+
{
14+
"name": "options",
15+
"type": "UseLazyOptions",
16+
"optional": true,
17+
"default": "{}",
18+
"description": "Configuration options for the lazy loading behavior",
19+
"properties": [
20+
{
21+
"name": "preload",
22+
"type": "boolean",
23+
"optional": true,
24+
"description": "Whether to preload the component immediately"
25+
},
26+
{
27+
"name": "onLoadStart",
28+
"type": "() => void",
29+
"optional": true,
30+
"description": "Callback when component starts loading"
31+
},
32+
{
33+
"name": "onLoadSuccess",
34+
"type": "(component: ComponentType<any>) => void",
35+
"optional": true,
36+
"description": "Callback when component loads successfully"
37+
},
38+
{
39+
"name": "onLoadError",
40+
"type": "(error: Error) => void",
41+
"optional": true,
42+
"description": "Callback when component fails to load"
43+
}
44+
]
45+
}
46+
],
47+
"returnType": {
48+
"type": "UseLazyReturn<T>",
49+
"properties": [
50+
{
51+
"name": "Component",
52+
"type": "T | null",
53+
"description": "The lazy component ready to be rendered",
54+
"category": "state"
55+
},
56+
{
57+
"name": "loading",
58+
"type": "boolean",
59+
"description": "Whether the component is currently loading",
60+
"category": "state"
61+
},
62+
{
63+
"name": "error",
64+
"type": "Error | null",
65+
"description": "Loading error if any occurred",
66+
"category": "state"
67+
},
68+
{
69+
"name": "load",
70+
"type": "() => Promise<T | null>",
71+
"description": "Manually trigger component loading",
72+
"category": "action"
73+
},
74+
{
75+
"name": "preload",
76+
"type": "() => Promise<T | null>",
77+
"description": "Preload the component without rendering it",
78+
"category": "action"
79+
},
80+
{
81+
"name": "reset",
82+
"type": "() => void",
83+
"description": "Reset the loading state and clear cached component",
84+
"category": "action"
85+
}
86+
]
87+
},
88+
"examples": [
89+
{
90+
"title": "Basic Lazy Loading",
91+
"description": "Simple lazy loading with Suspense boundary",
92+
"code": "import { Suspense } from 'react';\nimport { useLazy } from './use-lazy';\n\nfunction App() {\n const { Component } = useLazy(() => import('./HeavyComponent'));\n\n return (\n <Suspense fallback={<div>Loading...</div>}>\n <Component />\n </Suspense>\n );\n}"
93+
},
94+
{
95+
"title": "With Loading States",
96+
"description": "Manual control over loading with loading states",
97+
"code": "const { Component, loading, error, load } = useLazy(\n () => import('./Dashboard'),\n {\n onLoadStart: () => console.log('Loading started'),\n onLoadSuccess: () => console.log('Component loaded'),\n onLoadError: (err) => console.error('Load failed:', err)\n }\n);\n\nif (error) return <div>Error: {error.message}</div>;\nif (loading) return <div>Loading component...</div>;\n\nreturn (\n <div>\n <button onClick={load}>Load Dashboard</button>\n {Component && <Component />}\n </div>\n);"
98+
},
99+
{
100+
"title": "Preloading Components",
101+
"description": "Preload components for better performance",
102+
"code": "const { Component, preload } = useLazy(\n () => import('./ExpensiveChart'),\n { preload: true } // Preload immediately\n);\n\n// Or preload on user interaction\nconst handleMouseEnter = () => {\n preload(); // Preload on hover\n};\n\nreturn (\n <div onMouseEnter={handleMouseEnter}>\n <Suspense fallback={<ChartSkeleton />}>\n <Component />\n </Suspense>\n </div>\n);"
103+
},
104+
{
105+
"title": "Conditional Lazy Loading",
106+
"description": "Load components based on conditions",
107+
"code": "const { Component, load, loading } = useLazy(\n () => import('./AdminPanel')\n);\n\nconst [showAdmin, setShowAdmin] = useState(false);\n\nconst handleShowAdmin = async () => {\n setShowAdmin(true);\n await load(); // Ensure component is loaded\n};\n\nreturn (\n <div>\n <button onClick={handleShowAdmin} disabled={loading}>\n {loading ? 'Loading...' : 'Show Admin Panel'}\n </button>\n {showAdmin && Component && (\n <Suspense fallback={<div>Loading admin...</div>}>\n <Component />\n </Suspense>\n )}\n </div>\n);"
108+
}
109+
],
110+
"dependencies": ["react"],
111+
"imports": [
112+
"import { lazy, useState, useCallback, useRef, ComponentType } from 'react';"
113+
],
114+
"notes": [
115+
"Always wrap lazy components with Suspense boundary for proper loading states",
116+
"The hook caches loaded components to prevent re-loading on re-renders",
117+
"Preloading is useful for components that will likely be needed soon",
118+
"Error handling should be implemented both in the hook callbacks and with Error Boundaries",
119+
"The Component returned is compatible with React.lazy and Suspense"
120+
]
121+
}

packages/hooks/src/use-lazy/index.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client";
2+
3+
import { lazy, useState, useCallback, useRef, ComponentType } from "react";
4+
5+
export interface UseLazyOptions {
6+
/** Preload the component immediately */
7+
preload?: boolean;
8+
/** Callback when component starts loading */
9+
onLoadStart?: () => void;
10+
/** Callback when component loads successfully */
11+
onLoadSuccess?: (component: ComponentType<any>) => void;
12+
/** Callback when component fails to load */
13+
onLoadError?: (error: Error) => void;
14+
}
15+
16+
export interface UseLazyReturn<T extends ComponentType<any>> {
17+
/** The lazy component */
18+
Component: T | null;
19+
/** Whether the component is currently loading */
20+
loading: boolean;
21+
/** Loading error if any */
22+
error: Error | null;
23+
/** Manually trigger component loading */
24+
load: () => Promise<T | null>;
25+
/** Preload the component without rendering */
26+
preload: () => Promise<T | null>;
27+
/** Reset the loading state */
28+
reset: () => void;
29+
}
30+
31+
export function useLazy<T extends ComponentType<any>>(
32+
importFn: () => Promise<{ default: T } | T>,
33+
options: UseLazyOptions = {}
34+
): UseLazyReturn<T> {
35+
const [loading, setLoading] = useState(false);
36+
const [error, setError] = useState<Error | null>(null);
37+
const [Component, setComponent] = useState<T | null>(null);
38+
39+
const loadPromiseRef = useRef<Promise<T | null> | null>(null);
40+
const lazyComponentRef = useRef<T | null | React.LazyExoticComponent<T>>(
41+
null
42+
);
43+
const hasLoadedRef = useRef(false);
44+
45+
const load = useCallback(async (): Promise<T | null> => {
46+
// Return existing promise if already loading
47+
if (loadPromiseRef.current) {
48+
return loadPromiseRef.current;
49+
}
50+
51+
// Return cached component if already loaded
52+
if (hasLoadedRef.current && lazyComponentRef.current) {
53+
return lazyComponentRef.current as T;
54+
}
55+
56+
setLoading(true);
57+
setError(null);
58+
options.onLoadStart?.();
59+
60+
loadPromiseRef.current = (async () => {
61+
try {
62+
const module = await importFn();
63+
const component = "default" in module ? module.default : module;
64+
65+
lazyComponentRef.current = component as T;
66+
hasLoadedRef.current = true;
67+
setComponent(component as T);
68+
setLoading(false);
69+
options.onLoadSuccess?.(component as T);
70+
71+
return component as T;
72+
} catch (err) {
73+
const error =
74+
err instanceof Error ? err : new Error("Failed to load component");
75+
setError(error);
76+
setLoading(false);
77+
options.onLoadError?.(error);
78+
throw error;
79+
} finally {
80+
loadPromiseRef.current = null;
81+
}
82+
})();
83+
84+
return loadPromiseRef.current;
85+
}, [importFn, options]);
86+
87+
const preload = useCallback(async (): Promise<T | null> => {
88+
try {
89+
return await load();
90+
} catch (error) {
91+
// Preload failures are silent by default
92+
return null;
93+
}
94+
}, [load]);
95+
96+
const reset = useCallback(() => {
97+
setLoading(false);
98+
setError(null);
99+
setComponent(null);
100+
hasLoadedRef.current = false;
101+
lazyComponentRef.current = null;
102+
loadPromiseRef.current = null;
103+
}, []);
104+
105+
// Create lazy component that triggers loading
106+
const LazyComponent = useCallback(() => {
107+
if (!lazyComponentRef.current) {
108+
// Create a lazy component that will trigger our load function
109+
lazyComponentRef.current = lazy(async () => {
110+
const component = await load();
111+
return { default: component } as { default: T };
112+
});
113+
}
114+
return lazyComponentRef.current;
115+
}, [load]);
116+
117+
// Preload if requested
118+
useState(() => {
119+
if (options.preload) {
120+
preload();
121+
}
122+
});
123+
124+
return {
125+
Component: Component as T | null,
126+
loading,
127+
error,
128+
load,
129+
preload,
130+
reset,
131+
};
132+
}

packages/hooks/src/use-lazy/meta.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "use-lazy",
3+
"title": "useLazy",
4+
"description": "A hook for managing lazy-loaded React components with loading states, error handling, and preloading capabilities",
5+
"category": "performance",
6+
"dependencies": [],
7+
"devDependencies": [],
8+
"files": [
9+
{
10+
"name": "index.ts",
11+
"type": "hook"
12+
}
13+
]
14+
}

0 commit comments

Comments
 (0)