(props: VirtualListProps)
| 73 | /// All user-provided `attributes` are spread onto the container element. |
| 74 | #[component] |
| 75 | pub fn VirtualList(props: VirtualListProps) -> Element { |
| 76 | let VirtualListProps { |
| 77 | count, |
| 78 | buffer, |
| 79 | estimate_size, |
| 80 | render_item, |
| 81 | attributes, |
| 82 | } = props; |
| 83 | |
| 84 | let container_id = crate::use_unique_id(); |
| 85 | |
| 86 | // Create the Store — only holds mutable shared state |
| 87 | let state: Store<VirtualizerState> = use_store(|| VirtualizerState { |
| 88 | scroll_offset: 0, |
| 89 | viewport_size: 0, |
| 90 | is_scrolling: false, |
| 91 | item_size_cache: HashMap::new(), |
| 92 | scroll_adjustments: 0, |
| 93 | stable_total_size: None, |
| 94 | stable_measurement_count: None, |
| 95 | deferred_adjustments: 0, |
| 96 | }); |
| 97 | |
| 98 | // Measurements as a memo — recomputes when count or item_size_cache change. |
| 99 | // Read (not peeked) by the render body so the component re-renders when the |
| 100 | // memo invalidates; peeking a dirty memo returns stale data (Memo::peek does |
| 101 | // not check the dirty flag), which can yield out-of-bounds indices when |
| 102 | // `count` shrinks between renders. |
| 103 | let measurements: Memo<Vec<crate::r#virtual::types::VirtualItem>> = use_memo(move || { |
| 104 | let count = count(); |
| 105 | let isc = state.item_size_cache(); |
| 106 | let item_size_cache = isc.read(); |
| 107 | let estimate_cb = estimate_size.as_ref().map(|c| move |i: usize| c(i)); |
| 108 | compute_measurements( |
| 109 | count, |
| 110 | &item_size_cache, |
| 111 | estimate_cb.as_ref().map(|f| f as &dyn Fn(usize) -> u32), |
| 112 | ) |
| 113 | }); |
| 114 | |
| 115 | // Subscribe to scroll events via JS bridge |
| 116 | use_effect(move || { |
| 117 | let script = r#" |
| 118 | const container = document.getElementById(await dioxus.recv()); |
| 119 | if (!container) return; |
| 120 | |
| 121 | let scrollEndTimer = null; |
| 122 | let lastOffset = null; |
| 123 | let lastViewport = null; |
| 124 | let lastIsScrolling = null; |
| 125 | |
| 126 | function publish(isScrolling) { |
| 127 | const scroll = Math.round(container.scrollTop); |
| 128 | const viewport = Math.min(container.clientHeight, window.innerHeight) || 600; |
| 129 | // Deduplicate only if the full scroll state is unchanged. |
| 130 | if ( |
| 131 | scroll === lastOffset && |
| 132 | viewport === lastViewport && |
nothing calls this directly
no test coverage detected