Files
ttrpg_manager/src/lib/useDebouncedCallback.ts
T
NilsBriggen 9ecd817bc6 Phase 1: player character depth
- Inventory + currency + encumbrance vs carrying capacity
- Class resources with correct short/long/daily rest recovery (applyRest)
- Spellcasting: slots (+pact), known/prepared spells, derived DC + attack
- Defenses: 5e death saves/inspiration/exhaustion, pf2e dying/wounded/hero points
- Derived weapon attacks + passive perception via extended RulesSystem
- Dexie v2 migration backfills new fields; debounced save flushes on pagehide
- 9 new unit tests, new character-depth e2e; all green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:30:34 +02:00

53 lines
1.4 KiB
TypeScript

import { useEffect, useMemo, useRef } from 'react';
/**
* Returns a debounced wrapper around `fn` that also flushes on unmount AND on
* page hide (tab close / refresh), so the last edit is never lost — the old app
* silently dropped unsaved character changes on navigation.
*/
export function useDebouncedCallback<A extends unknown[]>(
fn: (...args: A) => void,
delay = 400,
): (...args: A) => void {
const fnRef = useRef(fn);
fnRef.current = fn;
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const pending = useRef<A | null>(null);
const flush = () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
if (pending.current) {
fnRef.current(...pending.current);
pending.current = null;
}
};
useEffect(() => {
const onHide = () => flush();
window.addEventListener('pagehide', onHide);
return () => {
window.removeEventListener('pagehide', onHide);
flush(); // flush on unmount
};
}, []);
return useMemo(
() =>
(...args: A) => {
pending.current = args;
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => {
timer.current = null;
if (pending.current) {
fnRef.current(...pending.current);
pending.current = null;
}
}, delay);
},
[delay],
);
}