leptos_shadcn_lazy_loading/
lib.rs1use leptos::prelude::*;
7use leptos::html::ElementChild;
8use leptos::task::spawn_local;
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::sync::Mutex;
12
13#[derive(Clone)]
15pub struct LazyComponentLoader {
16 components: Arc<Mutex<HashMap<String, ComponentLoader>>>,
17}
18
19pub type ComponentLoader = Box<dyn Fn() -> Result<View<()>, String> + Send + Sync>;
21
22impl LazyComponentLoader {
23 pub fn new() -> Self {
25 Self {
26 components: Arc::new(Mutex::new(HashMap::new())),
27 }
28 }
29
30 pub fn register_component<F>(&self, name: &str, loader: F)
32 where
33 F: Fn() -> Result<View<()>, String> + Send + Sync + 'static,
34 {
35 let mut components = self.components.lock().unwrap();
36 components.insert(name.to_string(), Box::new(loader));
37 }
38
39 pub fn load_component(&self, name: &str) -> Result<View<()>, String> {
41 let components = self.components.lock().unwrap();
42 if let Some(loader) = components.get(name) {
43 loader()
44 } else {
45 Err(format!("Component '{}' not found", name))
46 }
47 }
48
49 pub fn has_component(&self, name: &str) -> bool {
51 let components = self.components.lock().unwrap();
52 components.contains_key(name)
53 }
54
55 pub fn registered_components(&self) -> Vec<String> {
57 let components = self.components.lock().unwrap();
58 components.keys().cloned().collect()
59 }
60}
61
62impl Default for LazyComponentLoader {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68#[component]
70pub fn LazyComponent(
71 #[prop(into)] name: String,
72 #[prop(optional)] fallback: Option<Box<dyn Fn() -> AnyView + Send + Sync>>,
73 #[prop(optional)] error_fallback: Option<Box<dyn Fn(String) -> AnyView + Send + Sync>>,
74) -> impl IntoView {
75 let loader = use_context::<LazyComponentLoader>()
76 .expect("LazyComponentLoader not found in context");
77
78 let (component, set_component) = signal(None::<Result<View<()>, String>>);
79 let (loading, set_loading) = signal(true);
80 let (error, set_error) = signal(None::<String>);
81
82 Effect::new(move |_| {
84 let name = name.clone();
85 let loader = loader.clone();
86
87 spawn_local(async move {
88 set_loading.set(true);
89 set_error.set(None);
90
91 let result = loader.load_component(&name);
93
94 set_component.set(Some(result.clone()));
95 set_loading.set(false);
96
97 if let Err(err) = result {
98 set_error.set(Some(err));
99 }
100 });
101 });
102
103 move || {
105 if loading.get() {
106 if let Some(fallback_fn) = &fallback {
108 fallback_fn()
109 } else {
110 view! {
111 <div class="lazy-loading-fallback">
112 <div class="loading-spinner"></div>
113 <p>"Loading component..."</p>
114 </div>
115 }.into_any()
116 }
117 } else if let Some(Ok(comp)) = component.get() {
118 comp.into_any()
120 } else if let Some(err) = error.get() {
121 if let Some(error_fn) = &error_fallback {
123 error_fn(err)
124 } else {
125 view! {
126 <div class="lazy-loading-error">
127 <p class="error-message">"Failed to load component: {err}"</p>
128 <button
129 class="retry-button"
130 on:click=move |_| {
131 set_loading.set(true);
132 set_error.set(None);
133 }
134 >
135 "Retry"
136 </button>
137 </div>
138 }.into_any()
139 }
140 } else {
141 view! { <div></div> }.into_any()
143 }
144 }
145}
146
147pub fn use_lazy_component(name: &str) -> (ReadSignal<bool>, ReadSignal<Option<Result<View<()>, String>>>, WriteSignal<bool>) {
149 let (loading, set_loading) = signal(false);
150 let (component, set_component) = signal(None::<Result<View<()>, String>>);
151
152 let loader = use_context::<LazyComponentLoader>()
153 .expect("LazyComponentLoader not found in context");
154
155 let name = name.to_string();
156 let load = move || {
157 set_loading.set(true);
158
159 spawn_local(async move {
160 let result = loader.load_component(&name);
161 set_component.set(Some(result));
162 set_loading.set(false);
163 });
164 };
165
166 (loading, component, set_loading)
167}
168
169pub struct BundleAnalyzer;
171
172impl BundleAnalyzer {
173 pub fn analyze_usage(components: &[String]) -> BundleAnalysis {
175 let mut analysis = BundleAnalysis::new();
176
177 let form_components = ["input", "label", "checkbox", "radio-group", "select", "textarea", "form"];
179 let layout_components = ["card", "separator", "skeleton", "tabs"];
180 let interactive_components = ["button", "checkbox", "radio-group", "select", "switch", "tabs"];
181
182 let form_count = components.iter().filter(|c| form_components.contains(&c.as_str())).count();
183 let layout_count = components.iter().filter(|c| layout_components.contains(&c.as_str())).count();
184 let interactive_count = components.iter().filter(|c| interactive_components.contains(&c.as_str())).count();
185
186 analysis.form_components = form_count;
187 analysis.layout_components = layout_count;
188 analysis.interactive_components = interactive_count;
189 analysis.total_components = components.len();
190
191 if form_count > 4 {
193 analysis.recommendations.push("Consider lazy loading form components to reduce initial bundle size".to_string());
194 }
195
196 if layout_count > 3 {
197 analysis.recommendations.push("Layout components can be loaded on demand for better performance".to_string());
198 }
199
200 if interactive_count > 5 {
201 analysis.recommendations.push("Interactive components benefit from lazy loading for better UX".to_string());
202 }
203
204 analysis
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct BundleAnalysis {
211 pub form_components: usize,
212 pub layout_components: usize,
213 pub interactive_components: usize,
214 pub total_components: usize,
215 pub recommendations: Vec<String>,
216}
217
218impl BundleAnalysis {
219 fn new() -> Self {
220 Self {
221 form_components: 0,
222 layout_components: 0,
223 interactive_components: 0,
224 total_components: 0,
225 recommendations: Vec::new(),
226 }
227 }
228}
229
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_lazy_component_loader() {
235 let loader = LazyComponentLoader::new();
236
237 loader.register_component("test", || {
239 Ok(View::new(()))
240 });
241
242 assert!(loader.has_component("test"));
243 assert!(!loader.has_component("nonexistent"));
244
245 let components = loader.registered_components();
246 assert!(components.contains(&"test".to_string()));
247 }
248
249 #[test]
250 fn test_bundle_analyzer() {
251 let components = vec!["button".to_string(), "input".to_string(), "card".to_string()];
252 let analysis = BundleAnalyzer::analyze_usage(&components);
253
254 assert_eq!(analysis.total_components, 3);
255 assert_eq!(analysis.form_components, 1);
256 assert_eq!(analysis.layout_components, 1);
257 assert_eq!(analysis.interactive_components, 1);
258 }
259}