Skip to main content

yew_hooks/hooks/
use_virtual_list.rs

1use std::rc::Rc;
2use wasm_bindgen::prelude::*;
3use yew::prelude::*;
4
5/// State handle for the [`use_virtual_list`] hook.
6#[derive(Clone, PartialEq)]
7pub struct VirtualListItem<T> {
8    /// The item data
9    pub data: T,
10    /// The index in the original list
11    pub index: usize,
12    /// The top position in pixels
13    pub top: f64,
14    /// The height of the item in pixels
15    pub height: f64,
16}
17
18/// State handle for the [`use_virtual_list`] hook.
19pub struct UseVirtualListHandle<T> {
20    /// The visible items
21    pub visible_items: Vec<VirtualListItem<T>>,
22    /// The total height of all items
23    pub total_height: f64,
24    /// The start index of visible items
25    pub start_index: usize,
26    /// The end index of visible items
27    pub end_index: usize,
28    /// Function to scroll to a specific index
29    pub scroll_to: Rc<dyn Fn(usize)>,
30}
31
32impl<T> Clone for UseVirtualListHandle<T>
33where
34    T: Clone,
35{
36    fn clone(&self) -> Self {
37        Self {
38            visible_items: self.visible_items.clone(),
39            total_height: self.total_height,
40            start_index: self.start_index,
41            end_index: self.end_index,
42            scroll_to: self.scroll_to.clone(),
43        }
44    }
45}
46
47impl<T> PartialEq for UseVirtualListHandle<T>
48where
49    T: PartialEq,
50{
51    fn eq(&self, other: &Self) -> bool {
52        self.visible_items == other.visible_items
53            && self.total_height == other.total_height
54            && self.start_index == other.start_index
55            && self.end_index == other.end_index
56    }
57}
58
59/// A hook that provides virtual scrolling for large lists.
60///
61/// This hook calculates which items should be visible based on the scroll position
62/// and container height, improving performance for large lists.
63///
64/// # Example
65///
66/// ```rust
67/// # use yew::prelude::*;
68/// #
69/// use yew_hooks::prelude::*;
70///
71/// #[function_component(VirtualList)]
72/// fn virtual_list() -> Html {
73///     let items = (0..10000).collect::<Vec<_>>();
74///     let item_height = |index: usize| 50.0;
75///     let container = use_node_ref();
76///     let wrapper = use_node_ref();
77///     let overscan = 5;
78///
79///     let virtual_list = use_virtual_list(
80///         items,
81///         item_height,
82///         container.clone(),
83///         wrapper.clone(),
84///         overscan,
85///     );
86///
87///     html! {
88///         <>
89///             <div
90///                 ref={container}
91///                 style="height: 400px; overflow: auto;"
92///             >
93///                 <div ref={wrapper}>
94///                     {
95///                         for virtual_list.visible_items.iter().map(|item| {
96///                             html! {
97///                                 <div
98///                                     key={item.index}
99///                                     style={format!(
100///                                         "position: absolute; top: {}px; height: {}px; width: 100%;",
101///                                         item.top, item.height
102///                                     )}
103///                                 >
104///                                     { format!("Item {}", item.data) }
105///                                 </div>
106///                             }
107///                         })
108///                     }
109///                 </div>
110///             </div>
111///             <button onclick={let scroll_to = virtual_list.scroll_to.clone(); Callback::from(move |_| scroll_to(100))}>{"Scroll to 100"}</button>
112///         </>
113///     }
114/// }
115/// ```
116#[hook]
117pub fn use_virtual_list<T>(
118    items: Vec<T>,
119    item_height: fn(usize) -> f64,
120    container: NodeRef,
121    wrapper: NodeRef,
122    overscan: usize,
123) -> UseVirtualListHandle<T>
124where
125    T: Clone + PartialEq + 'static,
126{
127    let scroll_position = use_state(|| 0.0);
128    let container_height = use_state(|| 0.0);
129    let handle = use_state(|| UseVirtualListHandle {
130        visible_items: vec![],
131        total_height: 0.0,
132        start_index: 0,
133        end_index: 0,
134        scroll_to: Rc::new(|_| {}),
135    });
136
137    {
138        let items = items.clone();
139        let scroll_top_val = *scroll_position;
140        let container_height_val = *container_height;
141        let handle_clone = handle.clone();
142        let wrapper_clone = wrapper.clone();
143        let scroll_position_clone = scroll_position.clone();
144        let container_clone = container.clone();
145        use_effect_with(
146            (items, container_height_val, scroll_top_val, overscan),
147            move |(items, container_height, scroll_top, overscan)| {
148                let heights: Vec<f64> = (0..items.len()).map(item_height).collect();
149                let total_height = heights.iter().sum::<f64>();
150                let mut cumulative = 0.0;
151                let mut start_index = 0;
152                for (i, &h) in heights.iter().enumerate() {
153                    if cumulative + h > *scroll_top {
154                        start_index = i;
155                        break;
156                    }
157                    cumulative += h;
158                }
159                let start_cum = cumulative;
160                let mut end_index = start_index;
161                let mut current_cum = start_cum;
162                while current_cum < *scroll_top + *container_height && end_index < items.len() {
163                    current_cum += heights[end_index];
164                    end_index += 1;
165                }
166                end_index = end_index.min(items.len());
167                let visible_start = start_index;
168                let visible_end = end_index;
169                let render_start = visible_start.saturating_sub(*overscan);
170                let render_end = (visible_end + *overscan).min(items.len());
171                let visible_items = (render_start..render_end)
172                    .map(|index| {
173                        let top = heights[0..index].iter().sum::<f64>();
174                        VirtualListItem {
175                            data: items[index].clone(),
176                            index,
177                            top,
178                            height: heights[index],
179                        }
180                    })
181                    .collect();
182                let scroll_to = {
183                    let heights = heights.clone();
184                    let st_setter = scroll_position_clone.clone();
185                    let container = container_clone.clone();
186                    Rc::new(move |index: usize| {
187                        if index < heights.len() {
188                            let top = heights[0..index].iter().sum::<f64>();
189                            st_setter.set(top);
190                            if let Some(c) = container.get() {
191                                if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
192                                    e.set_scroll_top(top as i32);
193                                }
194                            }
195                        }
196                    })
197                };
198                let new_handle = UseVirtualListHandle {
199                    visible_items,
200                    total_height,
201                    start_index: visible_start,
202                    end_index: visible_end.saturating_sub(1),
203                    scroll_to,
204                };
205                handle_clone.set(new_handle.clone());
206                // Set height on wrapper
207                if let Some(w) = wrapper_clone.get() {
208                    if let Some(e) = w.dyn_ref::<web_sys::HtmlElement>() {
209                        let _ = e
210                            .style()
211                            .set_property("height", &format!("{}px", total_height));
212                        let _ = e.style().set_property("position", "relative");
213                    }
214                }
215            },
216        );
217    }
218
219    {
220        let container_clone = container.clone();
221        let scroll_position_clone = scroll_position.clone();
222        let container_height_clone = container_height.clone();
223        use_effect_with(container_clone, move |container| {
224            if let Some(c) = container.get() {
225                let c = c.clone();
226                if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
227                    container_height_clone.set(e.client_height() as f64);
228                    scroll_position_clone.set(e.scroll_top() as f64);
229                    let scroll_top_inner = scroll_position_clone.clone();
230                    let c_clone = c.clone();
231                    let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
232                        if let Some(e) = c_clone.dyn_ref::<web_sys::HtmlElement>() {
233                            scroll_top_inner.set(e.scroll_top() as f64);
234                        }
235                    }) as Box<dyn FnMut(_)>);
236                    let _ = e.add_event_listener_with_callback(
237                        "scroll",
238                        closure.as_ref().unchecked_ref(),
239                    );
240                    closure.forget();
241                }
242            }
243            || {}
244        });
245    }
246
247    (*handle).clone()
248}