Skip to content

Commit 98dca02

Browse files
authored
add use_virtual_list hook written by copilot (#52)
1 parent a90c1c6 commit 98dca02

8 files changed

Lines changed: 319 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ fn counter() -> Html {
8686
- `use_default` - returns the default value when state is None.
8787
- `use_debounce_state` - debounces state.
8888
- `use_throttle_state` - throttles state.
89+
- `use_virtual_list` - provides virtual scrolling for large lists to improve performance.
8990

9091
### Side-effects
9192

crates/yew-hooks/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "yew-hooks"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
edition = "2021"
55
authors = ["Jet Li <jing.i.qin@icloud.com>"]
66
categories = ["gui", "wasm", "web-programming"]
@@ -31,14 +31,17 @@ features = [
3131
"Blob",
3232
"CloseEvent",
3333
"Coordinates",
34+
"CssStyleDeclaration",
3435
"DataTransfer",
3536
"DataTransferItem",
3637
"DataTransferItemList",
3738
"DomRectReadOnly",
3839
"Element",
40+
"Event",
3941
"File",
4042
"Geolocation",
4143
"HtmlCollection",
44+
"HtmlElement",
4245
"HtmlLinkElement",
4346
"HtmlMediaElement",
4447
"IntersectionObserver",

crates/yew-hooks/src/hooks/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ mod use_title;
5151
mod use_toggle;
5252
mod use_unmount;
5353
mod use_update;
54+
mod use_virtual_list;
5455
mod use_visible;
5556
mod use_websocket;
5657
mod use_window_scroll;
@@ -109,6 +110,7 @@ pub use use_title::*;
109110
pub use use_toggle::*;
110111
pub use use_unmount::*;
111112
pub use use_update::*;
113+
pub use use_virtual_list::*;
112114
pub use use_visible::*;
113115
pub use use_websocket::*;
114116
pub use use_window_scroll::*;
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use std::rc::Rc;
2+
use wasm_bindgen::prelude::*;
3+
use yew::prelude::*;
4+
5+
/// State handle for the [`use_virtual_list`] hook.
6+
#[derive(Clone, PartialEq)]
7+
pub 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.
19+
pub 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+
32+
impl<T> Clone for UseVirtualListHandle<T>
33+
where
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+
47+
impl<T> PartialEq for UseVirtualListHandle<T>
48+
where
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]
117+
pub 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>
124+
where
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 start_index = start_index.saturating_sub(*overscan);
168+
let end_index = (end_index + *overscan).min(items.len());
169+
let visible_items = (start_index..end_index)
170+
.map(|index| {
171+
let top = heights[0..index].iter().sum::<f64>();
172+
VirtualListItem {
173+
data: items[index].clone(),
174+
index,
175+
top,
176+
height: heights[index],
177+
}
178+
})
179+
.collect();
180+
let scroll_to = {
181+
let heights = heights.clone();
182+
let st_setter = scroll_position_clone.clone();
183+
let container = container_clone.clone();
184+
Rc::new(move |index: usize| {
185+
if index < heights.len() {
186+
let top = heights[0..index].iter().sum::<f64>();
187+
st_setter.set(top);
188+
if let Some(c) = container.get() {
189+
if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
190+
e.set_scroll_top(top as i32);
191+
}
192+
}
193+
}
194+
})
195+
};
196+
let new_handle = UseVirtualListHandle {
197+
visible_items,
198+
total_height,
199+
start_index,
200+
end_index,
201+
scroll_to,
202+
};
203+
handle_clone.set(new_handle.clone());
204+
// Set height on wrapper
205+
if let Some(w) = wrapper_clone.get() {
206+
if let Some(e) = w.dyn_ref::<web_sys::HtmlElement>() {
207+
let _ = e
208+
.style()
209+
.set_property("height", &format!("{}px", total_height));
210+
let _ = e.style().set_property("position", "relative");
211+
}
212+
}
213+
},
214+
);
215+
}
216+
217+
{
218+
let container_clone = container.clone();
219+
let scroll_position_clone = scroll_position.clone();
220+
let container_height_clone = container_height.clone();
221+
use_effect_with(container_clone, move |container| {
222+
if let Some(c) = container.get() {
223+
let c = c.clone();
224+
if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
225+
container_height_clone.set(e.client_height() as f64);
226+
scroll_position_clone.set(e.scroll_top() as f64);
227+
let scroll_top_inner = scroll_position_clone.clone();
228+
let c_clone = c.clone();
229+
let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
230+
if let Some(e) = c_clone.dyn_ref::<web_sys::HtmlElement>() {
231+
scroll_top_inner.set(e.scroll_top() as f64);
232+
}
233+
}) as Box<dyn FnMut(_)>);
234+
let _ = e.add_event_listener_with_callback(
235+
"scroll",
236+
closure.as_ref().unchecked_ref(),
237+
);
238+
closure.forget();
239+
}
240+
}
241+
|| {}
242+
});
243+
}
244+
245+
(*handle).clone()
246+
}

examples/yew-app/src/app/home.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub fn home() -> Html {
2424
<li><Link<AppRoute> to={AppRoute::UseMutLatest} classes="text-emerald-800 underline">{ "use_mut_latest" }</Link<AppRoute>> { " - returns the latest mutable ref to state or props." }</li>
2525
<li><Link<AppRoute> to={AppRoute::UsePrevious} classes="text-emerald-800 underline">{ "use_previous" }</Link<AppRoute>> { " - returns the previous immutable ref to state or props." }</li>
2626
<li><Link<AppRoute> to={AppRoute::UseList} classes="text-emerald-800 underline">{ "use_list" }</Link<AppRoute>> { " - tracks state of a list." }</li>
27+
<li><Link<AppRoute> to={AppRoute::UseVirtualList} classes="text-emerald-800 underline">{ "use_virtual_list" }</Link<AppRoute>> { " - provides virtual scrolling for large lists." }</li>
2728
<li><Link<AppRoute> to={AppRoute::UseMap} classes="text-emerald-800 underline">{ "use_map" }</Link<AppRoute>> { " - tracks state of a hash map." }</li>
2829
<li><Link<AppRoute> to={AppRoute::UseSet} classes="text-emerald-800 underline">{ "use_set" }</Link<AppRoute>> { " - tracks state of a hash set." }</li>
2930
<li><Link<AppRoute> to={AppRoute::UseQueue} classes="text-emerald-800 underline">{ "use_queue" }</Link<AppRoute>> { " - tracks state of a queue." }</li>

examples/yew-app/src/app/hooks/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod use_title;
5353
mod use_toggle;
5454
mod use_unmount;
5555
mod use_update;
56+
mod use_virtual_list;
5657
mod use_visible;
5758
mod use_websocket;
5859
mod use_window_scroll;
@@ -113,6 +114,7 @@ pub use use_title::*;
113114
pub use use_toggle::*;
114115
pub use use_unmount::*;
115116
pub use use_update::*;
117+
pub use use_virtual_list::*;
116118
pub use use_visible::*;
117119
pub use use_websocket::*;
118120
pub use use_window_scroll::*;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use yew::prelude::*;
2+
use yew_hooks::prelude::*;
3+
4+
use crate::components::ui::button::Button;
5+
6+
/// `use_virtual_list` demo
7+
#[function_component]
8+
pub fn UseVirtualList() -> Html {
9+
let items = (0..10000).collect::<Vec<_>>();
10+
let item_height = |_index: usize| 50.0;
11+
let items_len = items.len();
12+
let container = use_node_ref();
13+
let wrapper = use_node_ref();
14+
let overscan = 5;
15+
16+
let virtual_list = use_virtual_list(
17+
items,
18+
item_height,
19+
container.clone(),
20+
wrapper.clone(),
21+
overscan,
22+
);
23+
24+
html! {
25+
<div class="container">
26+
<header class="mt-24 text-xl text-center">
27+
<div class="space-x-4 space-y-4">
28+
<p>{ format!("Total items: {}, Visible: {}-{}", items_len, virtual_list.start_index, virtual_list.end_index) }</p>
29+
<Button
30+
onclick={let scroll_to = virtual_list.scroll_to.clone(); Callback::from(move |_| scroll_to(100))}
31+
>
32+
{ "Scroll to 100" }
33+
</Button>
34+
<div
35+
ref={container}
36+
style="height: 400px; overflow: auto; border: 1px solid #ccc;"
37+
>
38+
<div ref={wrapper}>
39+
{
40+
for virtual_list.visible_items.iter().map(|item| {
41+
html! {
42+
<div
43+
key={item.index}
44+
style={format!(
45+
"position: absolute; top: {}px; height: {}px; width: 100%; border-bottom: 1px solid #eee; padding: 10px; box-sizing: border-box;",
46+
item.top, item.height
47+
)}
48+
>
49+
{ format!("Item {} (index: {})", item.data, item.index) }
50+
</div>
51+
}
52+
})
53+
}
54+
</div>
55+
</div>
56+
</div>
57+
</header>
58+
</div>
59+
}
60+
}

examples/yew-app/src/app/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ pub enum AppRoute {
129129
UseInfiniteScroll,
130130
#[at("/use_visible")]
131131
UseVisible,
132+
#[at("/use_virtual_list")]
133+
UseVirtualList,
132134
#[at("/use_hovered")]
133135
UseHovered,
134136
#[at("/use_permission")]
@@ -202,6 +204,7 @@ pub fn switch(routes: AppRoute) -> Html {
202204
AppRoute::UseClipboard => html! { <UseClipboard /> },
203205
AppRoute::UseInfiniteScroll => html! { <UseInfiniteScroll /> },
204206
AppRoute::UseVisible => html! { <UseVisible /> },
207+
AppRoute::UseVirtualList => html! { <UseVirtualList /> },
205208
AppRoute::UseHovered => html! { <UseHovered /> },
206209
AppRoute::UsePermission => html! { <UsePermission /> },
207210
AppRoute::PageNotFound => html! { <Home /> },

0 commit comments

Comments
 (0)