19d74d00d6
The synchronous pre-timeout in the implementation was a band-aid for a missing flushSync() in the third test. $effect callbacks are scheduled as microtasks and don't run until flushed — the test set source=true before the initial effect had a chance to register the setTimeout, so advanceTimersByTime fired nothing. Proper fix: call flushSync() after $effect.root so the initial effect runs synchronously, then advance fake timers. Removed the duplicate setTimeout branch from useDelayed — one code path, no race. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
38 lines
980 B
TypeScript
38 lines
980 B
TypeScript
// Returns a rune-wrapped boolean that mirrors the source getter, but only
|
|
// flips to true after the source has been true for delayMs continuously.
|
|
// Flips back to false immediately when the source becomes false.
|
|
//
|
|
// Used to suppress flash-of-skeleton on cache-hit navigation: skeletons
|
|
// only render for loads that take longer than ~100ms.
|
|
export function useDelayed(
|
|
source: () => boolean,
|
|
delayMs = 100
|
|
): { readonly value: boolean } {
|
|
let delayed = $state(false);
|
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
$effect(() => {
|
|
const v = source();
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
if (v) {
|
|
timeout = setTimeout(() => {
|
|
delayed = true;
|
|
timeout = null;
|
|
}, delayMs);
|
|
} else {
|
|
delayed = false;
|
|
}
|
|
return () => {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
}
|
|
};
|
|
});
|
|
|
|
return { get value() { return delayed; } };
|
|
}
|