Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ fn counter() -> Html {
- `use_default` - returns the default value when state is None.
- `use_debounce_state` - debounces state.
- `use_throttle_state` - throttles state.
- `use_virtual_list` - provides virtual scrolling for large lists to improve performance.

### Side-effects

Expand Down
5 changes: 4 additions & 1 deletion crates/yew-hooks/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "yew-hooks"
version = "0.4.0"
version = "0.4.1"
edition = "2021"
authors = ["Jet Li <jing.i.qin@icloud.com>"]
categories = ["gui", "wasm", "web-programming"]
Expand Down Expand Up @@ -31,14 +31,17 @@ features = [
"Blob",
"CloseEvent",
"Coordinates",
"CssStyleDeclaration",
"DataTransfer",
"DataTransferItem",
"DataTransferItemList",
"DomRectReadOnly",
"Element",
"Event",
"File",
"Geolocation",
"HtmlCollection",
"HtmlElement",
"HtmlLinkElement",
"HtmlMediaElement",
"IntersectionObserver",
Expand Down
2 changes: 2 additions & 0 deletions crates/yew-hooks/src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod use_title;
mod use_toggle;
mod use_unmount;
mod use_update;
mod use_virtual_list;
mod use_visible;
mod use_websocket;
mod use_window_scroll;
Expand Down Expand Up @@ -109,6 +110,7 @@ pub use use_title::*;
pub use use_toggle::*;
pub use use_unmount::*;
pub use use_update::*;
pub use use_virtual_list::*;
pub use use_visible::*;
pub use use_websocket::*;
pub use use_window_scroll::*;
Expand Down
246 changes: 246 additions & 0 deletions crates/yew-hooks/src/hooks/use_virtual_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use yew::prelude::*;

/// State handle for the [`use_virtual_list`] hook.
#[derive(Clone, PartialEq)]
pub struct VirtualListItem<T> {
/// The item data
pub data: T,
/// The index in the original list
pub index: usize,
/// The top position in pixels
pub top: f64,
/// The height of the item in pixels
pub height: f64,
}

/// State handle for the [`use_virtual_list`] hook.
pub struct UseVirtualListHandle<T> {
/// The visible items
pub visible_items: Vec<VirtualListItem<T>>,
/// The total height of all items
pub total_height: f64,
/// The start index of visible items
pub start_index: usize,
/// The end index of visible items
pub end_index: usize,
/// Function to scroll to a specific index
pub scroll_to: Rc<dyn Fn(usize)>,
}

impl<T> Clone for UseVirtualListHandle<T>
where
T: Clone,
{
fn clone(&self) -> Self {
Self {
visible_items: self.visible_items.clone(),
total_height: self.total_height,
start_index: self.start_index,
end_index: self.end_index,
scroll_to: self.scroll_to.clone(),
}
}
}

impl<T> PartialEq for UseVirtualListHandle<T>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.visible_items == other.visible_items
&& self.total_height == other.total_height
&& self.start_index == other.start_index
&& self.end_index == other.end_index
}
}

/// A hook that provides virtual scrolling for large lists.
///
/// This hook calculates which items should be visible based on the scroll position
/// and container height, improving performance for large lists.
///
/// # Example
///
/// ```rust
/// # use yew::prelude::*;
/// #
/// use yew_hooks::prelude::*;
///
/// #[function_component(VirtualList)]
/// fn virtual_list() -> Html {
/// let items = (0..10000).collect::<Vec<_>>();
/// let item_height = |index: usize| 50.0;
/// let container = use_node_ref();
/// let wrapper = use_node_ref();
/// let overscan = 5;
///
/// let virtual_list = use_virtual_list(
/// items,
/// item_height,
/// container.clone(),
/// wrapper.clone(),
/// overscan,
/// );
///
/// html! {
/// <>
/// <div
/// ref={container}
/// style="height: 400px; overflow: auto;"
/// >
/// <div ref={wrapper}>
/// {
/// for virtual_list.visible_items.iter().map(|item| {
/// html! {
/// <div
/// key={item.index}
/// style={format!(
/// "position: absolute; top: {}px; height: {}px; width: 100%;",
/// item.top, item.height
/// )}
/// >
/// { format!("Item {}", item.data) }
/// </div>
/// }
/// })
/// }
/// </div>
/// </div>
/// <button onclick={let scroll_to = virtual_list.scroll_to.clone(); Callback::from(move |_| scroll_to(100))}>{"Scroll to 100"}</button>
/// </>
/// }
/// }
/// ```
#[hook]
pub fn use_virtual_list<T>(
items: Vec<T>,
item_height: fn(usize) -> f64,
container: NodeRef,
wrapper: NodeRef,
overscan: usize,
) -> UseVirtualListHandle<T>
where
T: Clone + PartialEq + 'static,
{
let scroll_position = use_state(|| 0.0);
let container_height = use_state(|| 0.0);
let handle = use_state(|| UseVirtualListHandle {
visible_items: vec![],
total_height: 0.0,
start_index: 0,
end_index: 0,
scroll_to: Rc::new(|_| {}),
});

{
let items = items.clone();
let scroll_top_val = *scroll_position;
let container_height_val = *container_height;
let handle_clone = handle.clone();
let wrapper_clone = wrapper.clone();
let scroll_position_clone = scroll_position.clone();
let container_clone = container.clone();
use_effect_with(
(items, container_height_val, scroll_top_val, overscan),
move |(items, container_height, scroll_top, overscan)| {
let heights: Vec<f64> = (0..items.len()).map(item_height).collect();
let total_height = heights.iter().sum::<f64>();
let mut cumulative = 0.0;
let mut start_index = 0;
for (i, &h) in heights.iter().enumerate() {
if cumulative + h > *scroll_top {
start_index = i;
break;
}
cumulative += h;
}
let start_cum = cumulative;
let mut end_index = start_index;
let mut current_cum = start_cum;
while current_cum < *scroll_top + *container_height && end_index < items.len() {
current_cum += heights[end_index];
end_index += 1;
}
end_index = end_index.min(items.len());
let start_index = start_index.saturating_sub(*overscan);
let end_index = (end_index + *overscan).min(items.len());
let visible_items = (start_index..end_index)
.map(|index| {
let top = heights[0..index].iter().sum::<f64>();
VirtualListItem {
data: items[index].clone(),
index,
top,
height: heights[index],
}
})
.collect();
let scroll_to = {
let heights = heights.clone();
let st_setter = scroll_position_clone.clone();
let container = container_clone.clone();
Rc::new(move |index: usize| {
if index < heights.len() {
let top = heights[0..index].iter().sum::<f64>();
st_setter.set(top);
if let Some(c) = container.get() {
if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
e.set_scroll_top(top as i32);
}
}
}
})
};
let new_handle = UseVirtualListHandle {
visible_items,
total_height,
start_index,
end_index,
scroll_to,
};
handle_clone.set(new_handle.clone());
// Set height on wrapper
if let Some(w) = wrapper_clone.get() {
if let Some(e) = w.dyn_ref::<web_sys::HtmlElement>() {
let _ = e
.style()
.set_property("height", &format!("{}px", total_height));
let _ = e.style().set_property("position", "relative");
}
}
},
);
}

{
let container_clone = container.clone();
let scroll_position_clone = scroll_position.clone();
let container_height_clone = container_height.clone();
use_effect_with(container_clone, move |container| {
if let Some(c) = container.get() {
let c = c.clone();
if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
container_height_clone.set(e.client_height() as f64);
scroll_position_clone.set(e.scroll_top() as f64);
let scroll_top_inner = scroll_position_clone.clone();
let c_clone = c.clone();
let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
if let Some(e) = c_clone.dyn_ref::<web_sys::HtmlElement>() {
scroll_top_inner.set(e.scroll_top() as f64);
}
}) as Box<dyn FnMut(_)>);
let _ = e.add_event_listener_with_callback(
"scroll",
closure.as_ref().unchecked_ref(),
);
closure.forget();
}
}
|| {}
});
}

(*handle).clone()
}
1 change: 1 addition & 0 deletions examples/yew-app/src/app/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub fn home() -> Html {
<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>
<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>
<li><Link<AppRoute> to={AppRoute::UseList} classes="text-emerald-800 underline">{ "use_list" }</Link<AppRoute>> { " - tracks state of a list." }</li>
<li><Link<AppRoute> to={AppRoute::UseVirtualList} classes="text-emerald-800 underline">{ "use_virtual_list" }</Link<AppRoute>> { " - provides virtual scrolling for large lists." }</li>
<li><Link<AppRoute> to={AppRoute::UseMap} classes="text-emerald-800 underline">{ "use_map" }</Link<AppRoute>> { " - tracks state of a hash map." }</li>
<li><Link<AppRoute> to={AppRoute::UseSet} classes="text-emerald-800 underline">{ "use_set" }</Link<AppRoute>> { " - tracks state of a hash set." }</li>
<li><Link<AppRoute> to={AppRoute::UseQueue} classes="text-emerald-800 underline">{ "use_queue" }</Link<AppRoute>> { " - tracks state of a queue." }</li>
Expand Down
2 changes: 2 additions & 0 deletions examples/yew-app/src/app/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ mod use_title;
mod use_toggle;
mod use_unmount;
mod use_update;
mod use_virtual_list;
mod use_visible;
mod use_websocket;
mod use_window_scroll;
Expand Down Expand Up @@ -113,6 +114,7 @@ pub use use_title::*;
pub use use_toggle::*;
pub use use_unmount::*;
pub use use_update::*;
pub use use_virtual_list::*;
pub use use_visible::*;
pub use use_websocket::*;
pub use use_window_scroll::*;
Expand Down
60 changes: 60 additions & 0 deletions examples/yew-app/src/app/hooks/use_virtual_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use yew::prelude::*;
use yew_hooks::prelude::*;

use crate::components::ui::button::Button;

/// `use_virtual_list` demo
#[function_component]
pub fn UseVirtualList() -> Html {
let items = (0..10000).collect::<Vec<_>>();
let item_height = |_index: usize| 50.0;
let items_len = items.len();
let container = use_node_ref();
let wrapper = use_node_ref();
let overscan = 5;

let virtual_list = use_virtual_list(
items,
item_height,
container.clone(),
wrapper.clone(),
overscan,
);

html! {
<div class="container">
<header class="mt-24 text-xl text-center">
<div class="space-x-4 space-y-4">
<p>{ format!("Total items: {}, Visible: {}-{}", items_len, virtual_list.start_index, virtual_list.end_index) }</p>
<Button
onclick={let scroll_to = virtual_list.scroll_to.clone(); Callback::from(move |_| scroll_to(100))}
>
{ "Scroll to 100" }
</Button>
<div
ref={container}
style="height: 400px; overflow: auto; border: 1px solid #ccc;"
>
<div ref={wrapper}>
{
for virtual_list.visible_items.iter().map(|item| {
html! {
<div
key={item.index}
style={format!(
"position: absolute; top: {}px; height: {}px; width: 100%; border-bottom: 1px solid #eee; padding: 10px; box-sizing: border-box;",
item.top, item.height
)}
>
{ format!("Item {} (index: {})", item.data, item.index) }
</div>
}
})
}
</div>
</div>
</div>
</header>
</div>
}
}
3 changes: 3 additions & 0 deletions examples/yew-app/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ pub enum AppRoute {
UseInfiniteScroll,
#[at("/use_visible")]
UseVisible,
#[at("/use_virtual_list")]
UseVirtualList,
#[at("/use_hovered")]
UseHovered,
#[at("/use_permission")]
Expand Down Expand Up @@ -202,6 +204,7 @@ pub fn switch(routes: AppRoute) -> Html {
AppRoute::UseClipboard => html! { <UseClipboard /> },
AppRoute::UseInfiniteScroll => html! { <UseInfiniteScroll /> },
AppRoute::UseVisible => html! { <UseVisible /> },
AppRoute::UseVirtualList => html! { <UseVirtualList /> },
AppRoute::UseHovered => html! { <UseHovered /> },
AppRoute::UsePermission => html! { <UsePermission /> },
AppRoute::PageNotFound => html! { <Home /> },
Expand Down