feat: Added debouncing to the browser search input

This commit is contained in:
LynithDev 2026-07-21 13:51:47 +02:00
parent 5929e51920
commit fbf2dc29b5
3 changed files with 45 additions and 3 deletions

View file

@ -0,0 +1,31 @@
use std::time::Duration;
use freya::prelude::*;
/// Returns a debounced mirror of `value`. The returned state only updates to a
/// new `value` once `delay` has elapsed without any further change. Rapid
/// changes (e.g. per-keystroke) collapse into a single trailing update.
pub fn use_debounced<T>(value: T, delay: Duration) -> State<T>
where
T: Clone + PartialEq + 'static,
{
let debounced = use_state(|| value.clone());
// Bumped on every change; the async task commits only if its generation is
// still the latest when the delay expires, dropping superseded changes.
let mut generation = use_state(|| 0u64);
use_side_effect_with_deps(&value, move |value| {
let value = value.clone();
let this_gen = *generation.peek() + 1;
generation.set(this_gen);
let mut debounced = debounced;
spawn(async move {
tokio::time::sleep(delay).await;
if *generation.peek() == this_gen {
debounced.set(value);
}
});
});
debounced
}

View file

@ -1,8 +1,10 @@
mod active_cluster;
mod debounce;
mod dispatch;
mod queries;
mod view_state;
pub use debounce::use_debounced;
pub use view_state::{PersistedView, use_view_state};
pub use active_cluster::{

View file

@ -1,3 +1,5 @@
use std::time::Duration;
use freya::animation::{
AnimNum, Ease, Function, OnChange, OnCreation, use_animation_with_dependencies,
};
@ -12,7 +14,7 @@ use crate::components::{
};
use crate::hooks::{
BROWSE_PAGE_SIZE, BrowserUiState, category_list, content_type_for_slug, search_items,
search_pending, search_total, use_browser_compat, use_browser_state_store,
search_pending, search_total, use_browser_compat, use_browser_state_store, use_debounced,
use_package_categories, use_package_search, use_view_state,
};
use crate::theme::colors;
@ -37,6 +39,7 @@ const BANNER_H: f32 = 100.;
const LIST_ROW_H: f32 = 78.;
const GRID_SPACING: f32 = 12.;
const LIST_SPACING: f32 = 8.;
const SEARCH_DEBOUNCE_MS: u64 = 250;
fn type_title(package_type: &str) -> &'static str {
match package_type {
@ -87,6 +90,12 @@ impl Component for Browser {
});
}
// Debounce the search query so typing doesn't fire a request per keystroke.
let debounced_query = use_debounced(
query.read().clone(),
Duration::from_millis(SEARCH_DEBOUNCE_MS),
);
let cluster = load_cluster(cluster_id);
let provider_id = *provider.read();
let compat = *compatible_only.read();
@ -107,7 +116,7 @@ impl Component for Browser {
let mut page_state = page;
let signature = format!(
"{provider_id:?}|{}|{compat}|{}",
query.read().trim().to_lowercase(),
debounced_query.read().trim().to_lowercase(),
cats.join(",")
);
let mut last_signature = use_state(|| signature.clone());
@ -121,7 +130,7 @@ impl Component for Browser {
let search = use_package_search(
provider_id,
content_type,
query.read().clone(),
debounced_query.read().clone(),
game_versions,
loaders,
cats.clone(),