DelayedIndicator
View as MarkdownDelays showing a transient value long enough to avoid flicker, and once shown, keeps it visible for a minimum duration.
DelayedIndicator<T> wraps any value, not just a boolean loading flag. Feed it every new value through set(); values you mark as "transient" are held back briefly before appearing and, once shown, stay for a minimum duration - everything else passes through immediately.
import { DelayedIndicator } from 'fluid-primitives';
const indicator = new DelayedIndicator({
isTransient: value => value === true,
onChange: visible => {
spinnerEl.hidden = !visible;
},
});
async function loadResults(query: string) {
indicator.set(true);
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const results = await response.json();
renderResults(results);
} finally {
indicator.set(false);
}
}
Call indicator.destroy() when it's no longer needed (e.g. on component teardown) to cancel any in-flight timer.
The options of a DelayedIndicator.
| Name | Description | Required | Default |
|---|---|---|---|
showDelayMs | numberHow long a transient value must persist before it is shown at all. Must be finite and >= 0. | No | 150 |
minVisibleMs | numberOnce shown, how long a transient value stays current before a settled value can replace it. Must be finite and >= 0. | No | 300 |
isTransient | (value: T) => booleanMarks which values are transient, e.g. a loading state. Every other value is treated as settled and passes through immediately once no transient value is held. | Yes | - |
onChange | (value: T) => voidCalled with each value once it is ready to be shown. | Yes | - |
Behavior
Permalink to heading "Behavior"- A transient value is held for
showDelayMsbeforeonChangefirst receives it. - Once shown, it stays current for at least
minVisibleMs, even if a settled value arrives sooner. - A settled value that arrives before any transient value has been shown is passed to
onChangeimmediately. - A settled value that arrives while a transient value is being shown waits for that value's
minVisibleMsfloor, then is passed toonChange. - A transient value arriving while one is already shown updates the displayed content immediately, without resetting the
minVisibleMsclock. set()does not deduplicate - passing the same value again may still result in anotheronChangecall, since this is a temporal filter on state rather than adistinctUntilChanged()operator.
Driving multiple UI elements from one instance
Permalink to heading "Driving multiple UI elements from one instance"Because isTransient/onChange work on any value type, one DelayedIndicator can keep several pieces of UI in sync - for example a spinner and a status message that should always change together. From the Combobox Async Search example:
type SearchStatus = 'loading' | 'error' | 'empty' | 'idle' | 'results';
const status = new DelayedIndicator<SearchStatus>({
isTransient: s => s === 'loading',
onChange: renderStatus, // toggles both the spinner and the status text together
});
status.set(getSearchStatus(api, hasResults));
renderStatus reads the single incoming status value to update both the spinner's visibility and the status text, so the two are always derived from the same source and change in lockstep.
Delays showing a transient value long enough to avoid flicker, and keeps it visible for a minimum duration once shown. Feed it every new value through set(), typically straight from a subscribe callback, and it forwards them to onChange. The values isTransient marks are held back for showDelayMs, so a fast operation never flashes them. Once shown they stay for at least minVisibleMs, so they can't vanish the instant they appeared.
It works on any value, not only a boolean: one instance can drive a spinner and a status text from the same source, so they never disagree.
class DelayedIndicator<T>
Constructor
Permalink to heading "Constructor"| Name | Description | Required | Default |
|---|---|---|---|
options | DelayedIndicatorOptions<T> | Yes | - |
set()
Passes a new value on, delayed as described above. It does not deduplicate: the same value again can still call onChange again, as this filters state over time, it is not a distinctUntilChanged().
set(value: T): void
| Name | Description | Required | Default |
|---|---|---|---|
value | TThe new value. | Yes | - |
destroy()
Cancels any timer that is running. Call it when the indicator is no longer needed, e.g. on teardown: otherwise a scheduled value can still reach onChange afterwards. It is safe to call when nothing is transient or shown.
destroy(): void