Skip to main content

leptos_shadcn_lazy_loading/
lib.rs

1//! Lazy loading system for shadcn/ui Leptos components
2//! 
3//! This module provides lazy loading capabilities to reduce initial bundle size
4//! by loading components only when they're needed.
5
6use 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/// Lazy component loader that manages dynamic imports
14#[derive(Clone)]
15pub struct LazyComponentLoader {
16    components: Arc<Mutex<HashMap<String, ComponentLoader>>>,
17}
18
19/// Component loader function type
20pub type ComponentLoader = Box<dyn Fn() -> Result<View<()>, String> + Send + Sync>;
21
22impl LazyComponentLoader {
23    /// Create a new lazy component loader
24    pub fn new() -> Self {
25        Self {
26            components: Arc::new(Mutex::new(HashMap::new())),
27        }
28    }
29
30    /// Register a component for lazy loading
31    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    /// Load a component by name
40    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    /// Check if a component is registered
50    pub fn has_component(&self, name: &str) -> bool {
51        let components = self.components.lock().unwrap();
52        components.contains_key(name)
53    }
54
55    /// Get all registered component names
56    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/// Lazy component wrapper that loads components on demand
69#[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    // Load component when name changes
83    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            // Simulate async loading
92            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    // Render based on state
104    move || {
105        if loading.get() {
106            // Show fallback while loading
107            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            // Component loaded successfully
119            comp.into_any()
120        } else if let Some(err) = error.get() {
121            // Component failed to load
122            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            // No component loaded yet
142            view! { <div></div> }.into_any()
143        }
144    }
145}
146
147/// Hook for lazy loading components
148pub 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
169/// Component bundle analyzer for optimization
170pub struct BundleAnalyzer;
171
172impl BundleAnalyzer {
173    /// Analyze component usage and provide optimization suggestions
174    pub fn analyze_usage(components: &[String]) -> BundleAnalysis {
175        let mut analysis = BundleAnalysis::new();
176        
177        // Analyze component categories
178        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        // Generate recommendations
192        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/// Bundle analysis results
209#[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        // Register a test component
238        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}