leptos_windowing/
hook.rs

1use std::{fmt::Debug, ops::Range};
2
3use leptos::prelude::*;
4
5use crate::{InternalLoader, ItemWindow, cache::Cache};
6
7/// Load items on demand and cache them.
8///
9/// Underlying functionality of [`use_pagination`] and [`use_virtualization`].
10/// You most probably don't want to use this directly but either [`use_pagination`] or [`use_virtualization`].
11///
12/// ## Params
13/// - `load_range`: A signal of the range of items to load. This has to include the `display_range`. Control the range of items to load and cache.
14/// - `display_range`: A signal of the range of items to display. This will be used for the returned `ItemWindow`.
15/// - `loader`: The loader to use for loading items.
16/// - `query`: A signal of the query to use for loading items.
17///
18/// ## Returns
19///
20/// A tuple containing:
21/// - `Signal<Result<Option<usize>, E>>`: A signal of the total number of items.
22///   This will be either:
23///   - `Ok(Some(n))`: The total number of items.
24///   - `Ok(None)`: The total number of items is unknown.
25///   - `Err(e)`: An error occurred while loading the total number of items.
26/// - `ItemWindow<T>`: A window of items that can be used to render a list/table of items.
27#[must_use]
28pub fn use_load_on_demand<T, L, Q, E, M>(
29    range_to_load: impl Into<Signal<Range<usize>>>,
30    range_to_display: impl Into<Signal<Range<usize>>>,
31    loader: L,
32    query: impl Into<Signal<Q>>,
33) -> UseLoadOnDemandResult<T, E>
34where
35    T: Send + Sync + 'static,
36    L: InternalLoader<M, Item = T, Query = Q, Error = E> + 'static,
37    Q: Send + Sync + 'static,
38    E: Send + Sync + Debug + 'static,
39{
40    #[cfg(not(feature = "ssr"))]
41    {
42        use crate::cache::CacheStoreFields;
43        use leptos::task::spawn_local;
44
45        let range_to_load = range_to_load.into();
46        let range_to_display = range_to_display.into();
47
48        let cached_range_to_display = RwSignal::new(0..0);
49
50        let cache = Cache::new_store();
51
52        let loader = Signal::stored_local(loader);
53        let query = query.into();
54
55        let item_count_result = RwSignal::new(Ok(None));
56
57        let set_item_count = move |count: Result<Option<usize>, E>| {
58            cache
59                .item_count()
60                .set(count.as_ref().ok().flatten().copied());
61            item_count_result.set(count);
62        };
63
64        let reload_counter = RwSignal::new(0_usize);
65
66        // Clear cache
67        Effect::new(move || {
68            query.track();
69            Cache::clear(cache);
70            reload_counter.update(|counter| *counter = counter.wrapping_add(1));
71        });
72
73        // Load item count
74        Effect::new(move || {
75            // we don't need to track the query here because it triggers cache invalidation which triggers reload_trigger
76
77            reload_counter.track();
78
79            spawn_local(async move {
80                let latest_reload_count = reload_counter.try_get_untracked();
81
82                let count = loader.read().item_count(&*query.read_untracked()).await;
83
84                // make sure the loaded count is still valid
85                if latest_reload_count == reload_counter.try_get_untracked() {
86                    set_item_count(count);
87                }
88            });
89        });
90
91        // Load items
92        Effect::new(move || {
93            // we don't need to track the query here because it triggers cache invalidation which triggers reload_trigger
94            reload_counter.track();
95
96            let missing_range = cache.read().missing_range(range_to_load.get());
97
98            if let Some(missing_range) = missing_range {
99                Cache::write_loading(cache, missing_range.clone());
100
101                spawn_local(async move {
102                    let latest_reload_count = reload_counter.try_get_untracked();
103
104                    let result = loader
105                        .read()
106                        .load_items(missing_range.clone(), &*query.read_untracked())
107                        .await;
108
109                    // make sure the loaded data is still valid
110                    if latest_reload_count == reload_counter.try_get_untracked() {
111                        if let Ok(loaded_items) = &result
112                            && loaded_items.range.end < missing_range.end
113                        {
114                            set_item_count(Ok(Some(loaded_items.range.end)));
115                        }
116
117                        Cache::write_loaded(
118                            cache,
119                            result.map_err(|e| format!("{e:?}")),
120                            missing_range,
121                        );
122                    }
123                });
124            }
125
126            // Make sure that the cache is filled and then update the display range
127            let Range { start, end } = range_to_display.get();
128            cached_range_to_display
129                .set(start..end.min(cache.item_count().get().unwrap_or(usize::MAX)));
130        });
131
132        UseLoadOnDemandResult {
133            item_count_result: item_count_result.into(),
134            item_window: ItemWindow {
135                cache,
136                range: cached_range_to_display.into(),
137            },
138        }
139    }
140
141    #[cfg(feature = "ssr")]
142    {
143        let _ = range_to_load;
144        let _ = range_to_display;
145        let _ = loader;
146        let _ = query;
147
148        UseLoadOnDemandResult {
149            item_count_result: Signal::stored(Ok(None)),
150            item_window: ItemWindow {
151                cache: Cache::new_store(),
152                range: Signal::stored(0..0),
153            },
154        }
155    }
156}
157
158/// Return type of [`use_load_on_demand`].
159pub struct UseLoadOnDemandResult<T, E>
160where
161    T: Send + Sync + 'static,
162    E: Send + Sync + Debug + 'static,
163{
164    pub item_count_result: Signal<Result<Option<usize>, E>>,
165    pub item_window: ItemWindow<T>,
166}