-
-
Notifications
You must be signed in to change notification settings - Fork 0
Hooks
defineHook({ name?, layers?, setup })Status: Current.
Hooks package reusable reactive logic. Component and hook layer access uses the same alias-record model.
export const useCurrentUser = defineHook({
name: 'useCurrentUser',
layers: { auth: AuthLayer } as const,
setup({ computed, layers }) {
const auth = layers.auth.service('auth');
return {
label: computed(() => auth.currentUser().name),
};
},
});The layer must already be registered at the application composition root. Missing bindings fail before hook setup executes.
| Member | Purpose |
|---|---|
config |
Typed input supplied by the caller. |
signal, computed
|
Reactive state and derivation. |
watchEffect |
Lifecycle-owned reactive effect with rerun cleanup and an EffectHandle. |
onMount |
Register work with an active component lifecycle. |
layers |
Typed alias or list accessor. |
scope |
Register LIFO finalizers or manually dispose a standalone hook. |
use |
Compose another hook function. |
runAsync |
Execute typed asynchronous work. |
const useThreshold = defineHook<
{ minimum: number },
{ accepted: (value: number) => boolean }
>({
setup({ config }) {
return { accepted: (value) => value >= config.minimum };
},
});Use Config | undefined when a hook has optional configuration. defineHook
then exposes an optional call instead of forcing callers to pass undefined:
const useFeatureFlag = defineHook<
{ initialValue: boolean } | undefined,
ReadonlySignal<boolean>
>({
setup({ config, signal }) {
return signal(config?.initialValue ?? false);
},
});
useFeatureFlag();
useFeatureFlag({ initialValue: true });Hooks called during component setup are owned by that component lifecycle.
Unmount stops every hook effect synchronously before asynchronous scope
finalizers run. A cleanup returned from watchEffect runs before the next
reactive execution and again when the effect stops:
const useWindowEvents = defineHook({
setup({ watchEffect }) {
const effect = watchEffect(() => {
const onResize = () => console.log(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
});
return { pause: effect.pause, resume: effect.resume };
},
});Use scope.addFinalizer for a resource that must survive effect reruns and end
only when the hook is disposed. Finalizers execute once in reverse registration
order. Every finalizer is attempted; multiple failures are combined and routed
through the lifecycle error handler.
When a hook is invoked without an active component lifecycle, onMount runs
immediately and its cleanup belongs to the hook scope. A standalone hook must
expose or otherwise arrange scope.dispose() if it allocates long-lived
resources. Component-owned hooks need no manual disposal.
If layer validation or hook setup throws, Effuse stops effects immediately, starts best-effort scope cleanup, preserves the original setup error, and reports any asynchronous rollback failure as a lifecycle cleanup error.
@effuse/use provides browser-focused hooks for storage, media queries, online
state, intervals, debounce/throttle, event listeners, element visibility,
window size, and related concerns. Browser-only hooks must retain explicit SSR
guards.
Debounce and throttle preserve the source signal value type, including unions, readonly values, and object shapes. Event listeners derive valid event names and callback payloads from their target:
useEventListener({
target: document,
event: 'visibilitychange',
handler: (event) => console.log(event.type), // Event
});
useEventListener({
target: button,
event: 'click',
handler: (event) => console.log(event.clientX), // MouseEvent
});Known DOM targets reject incompatible event names during type checking. Custom
EventTarget implementations accept application-defined string event names
and receive Event.
Effect may power internal hook implementation, but it is not part of the
@effuse/use developer contract. Published ESM and CommonJS declarations use
Effuse-owned tagged unions, constructors, matchers, and native Error shapes;
they contain no imports from Effect. A package build gate enforces this
boundary. Developers can construct and narrow states, use $is and $match,
and catch exported errors without importing or learning Effect.
The first production browser-utility set shares defineHook lifecycle
ownership, explicit SSR state, typed failures, tracing categories, and
Effect-free declarations.
| Hook | Production contract |
|---|---|
useTimeout |
Deadline-based remaining time, pause/resume, restart, cancel, exact-once completion, and callback error state. |
useDocumentVisibility |
visible, hidden, or explicit unknown state with mount synchronization and listener cleanup. |
useClipboard |
Independent read/write capabilities, best-effort permissions, non-rejecting operations, and race-safe transient copied state. |
usePreferredColorScheme |
Separate dark/light queries preserving no-preference, with explicit SSR and unsupported state. |
const timeout = useTimeout({
delay: 5_000,
immediate: false,
callback: () => dismissNotice(),
});
timeout.start(); // Start or resume a paused timeout.
timeout.pause();
timeout.restart(); // Reset to the full configured delay.
timeout.cancel(); // Clear work and return to idle.Remaining time is calculated from an absolute deadline rather than assumed timer intervals, so background throttling does not accumulate arithmetic drift. Automatic start occurs on mount. Timers cannot fire after unmount.
const visibility = useDocumentVisibility();
const colors = usePreferredColorScheme();Both hooks default to unknown during SSR and before mount. Applications with
a deliberate hydration assumption can pass ssrState or ssrScheme.
Unsupported clients return unknown with isSupported set to false.
usePreferredColorScheme returns light, dark, no-preference, or
unknown. A false dark query is not treated as proof of light preference.
const clipboard = useClipboard({ copiedDuration: 2_000 });
const copied = await clipboard.copy('Effuse');
const text = await clipboard.read();canRead and canWrite are independent. Permission querying is best-effort
because browser support differs. Unsupported APIs, denied permissions, and
read/write failures resolve to false or null and populate typed
ClipboardError state; expected capability failures do not reject into
component code. Late asynchronous results, permission listeners, and copied
timers are discarded on unmount.