Skip to content

Commit 93209fb

Browse files
committed
refactor: Add useDebounce and usePersistedState wrappers
1 parent a6c673f commit 93209fb

7 files changed

Lines changed: 42 additions & 11 deletions

File tree

src/lib/components/input/PasswordInput.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { validatePassword } from '$lib/inputvalidation/passwordValidator';
88
import type { AnyComponent } from '$lib/types/AnyComponent';
99
import type { ValidationResult } from '$lib/types/ValidationResult';
10-
import { debounce } from '$lib/utils/debounce';
10+
import { useDebounce } from '$lib/utils/debounce';
1111
import type { Snippet } from 'svelte';
1212
import type { FullAutoFill } from 'svelte/elements';
1313
import PasswordStrengthMeter from './impl/PasswordStrengthMeter.svelte';
@@ -42,7 +42,7 @@
4242
4343
let validationResult = $state<ValidationResult | null>(null);
4444
45-
const requestHIBP = debounce(async (str: string) => {
45+
const requestHIBP = useDebounce(async (str: string) => {
4646
try {
4747
const pwnedCount = await checkPwnedCount(str);
4848
if (pwnedCount > 0) {

src/lib/components/input/UsernameInput.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
} from '$lib/inputvalidation/usernameValidator';
1212
import type { AnyComponent } from '$lib/types/AnyComponent';
1313
import type { ValidationResult } from '$lib/types/ValidationResult';
14-
import { debounce } from '$lib/utils/debounce';
14+
import { useDebounce } from '$lib/utils/debounce';
1515
import type { Snippet } from 'svelte';
1616
import type { FullAutoFill } from 'svelte/elements';
1717
@@ -40,7 +40,7 @@
4040
let checkResponses = $state<Map<string, ValidationResult>>(new Map());
4141
let validationResult = $state<ValidationResult | null>(null);
4242
43-
const requestAvailability = debounce(async (username: string) => {
43+
const requestAvailability = useDebounce(async (username: string) => {
4444
try {
4545
const response = await accountV2Api.accountCheckUsername({ username });
4646
validationResult = mapUsernameCheckResponse(response);

src/lib/state/classes/persisted-state.svelte.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { browser } from '$app/environment';
2+
import { onDestroy } from 'svelte';
23

34
export type StorageType = 'local' | 'session';
45

@@ -39,6 +40,13 @@ export class PersistedState<T> {
3940
}
4041
}
4142

43+
/** Detach the cross-tab `storage` listener. No-op if none was attached. */
44+
dispose() {
45+
if (this.#storage === localStorage) {
46+
if (browser) window.removeEventListener('storage', this.#onStorage);
47+
}
48+
}
49+
4250
#onStorage = (event: StorageEvent) => {
4351
if (event.storageArea !== this.#storage) return;
4452
if (event.key !== this.#key) return;
@@ -75,3 +83,14 @@ export class PersistedState<T> {
7583
return JSON.parse(raw) as T;
7684
}
7785
}
86+
87+
/** Component-aware `PersistedState` that detaches the storage listener on unmount. */
88+
export function usePersistedState<T>(
89+
key: string,
90+
defaultValue: T,
91+
options?: LocalStorageStateOptions
92+
): PersistedState<T> {
93+
const state = new PersistedState(key, defaultValue, options);
94+
onDestroy(() => state.dispose());
95+
return state;
96+
}

src/lib/utils/debounce.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { onDestroy } from 'svelte';
2+
13
export interface Debounced<Args extends unknown[]> {
24
(...args: Args): void;
35
cancel(): void;
@@ -39,3 +41,13 @@ export function debounce<Args extends unknown[]>(
3941

4042
return debounced;
4143
}
44+
45+
/** Component-aware `debounce` that cancels any pending invocation on unmount. */
46+
export function useDebounce<Args extends unknown[]>(
47+
fn: (...args: Args) => void,
48+
delay: number
49+
): Debounced<Args> {
50+
const d = debounce(fn, delay);
51+
onDestroy(d.cancel);
52+
return d;
53+
}

src/routes/(app)/admin/blacklists/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import { Separator } from '$lib/components/ui/separator';
1616
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';
1717
import type { ValidationResult } from '$lib/types/ValidationResult';
18-
import { debounce } from '$lib/utils/debounce';
18+
import { useDebounce } from '$lib/utils/debounce';
1919
2020
registerBreadcrumbs(() => [{ label: 'Blacklists' }]);
2121
@@ -139,7 +139,7 @@
139139
}
140140
}
141141
142-
const debouncedLoadUsernames = debounce(loadUsernames, 400);
142+
const debouncedLoadUsernames = useDebounce(loadUsernames, 400);
143143
$effect(() => {
144144
if (usernameEntry.length == 0) {
145145
debouncedLoadUsernames.cancel();
@@ -150,7 +150,7 @@
150150
debouncedLoadUsernames();
151151
});
152152
153-
const debouncedLoadEmails = debounce(loadEmails, 400);
153+
const debouncedLoadEmails = useDebounce(loadEmails, 400);
154154
$effect(() => {
155155
if (emailEntry.length == 0) {
156156
debouncedLoadEmails.cancel();

src/routes/(app)/admin/users/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
import { CardHeader, CardTitle } from '$lib/components/ui/card';
8686
import { Input } from '$lib/components/ui/input';
8787
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';
88-
import { debounce } from '$lib/utils/debounce';
88+
import { useDebounce } from '$lib/utils/debounce';
8989
9090
let isFetching = $state(false);
9191
@@ -117,7 +117,7 @@
117117
page = Math.floor(response.offset / response.limit) + 1;
118118
}
119119
120-
const applyFilterQuery = debounce((query: string | undefined) => (filterQuery = query), 800);
120+
const applyFilterQuery = useDebounce((query: string | undefined) => (filterQuery = query), 800);
121121
$effect(() => {
122122
const queries: string[] = [];
123123

src/routes/+layout.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import Sidebar from './Sidebar.svelte';
1212
import '../app.css';
1313
import { IsMobile } from '$lib/hooks/is-mobile.svelte';
14-
import { PersistedState } from '$lib/state/classes/persisted-state.svelte';
14+
import { usePersistedState } from '$lib/state/classes/persisted-state.svelte';
1515
import DialogManager from '$lib/components/dialog-manager/dialog-manager.svelte';
1616
1717
interface Props {
@@ -23,7 +23,7 @@
2323
let meta = $derived(buildMetaData(page.url));
2424
2525
const mobile = new IsMobile();
26-
const sidebarOpen = new PersistedState('sidebarOpen', false);
26+
const sidebarOpen = usePersistedState('sidebarOpen', false);
2727
const isOpen = $derived(mobile.current ? false : sidebarOpen.value);
2828
</script>
2929

0 commit comments

Comments
 (0)