Skip to content

Services

A service is a shared source of props that components subscribe to. Every one of them is lazy and reference-counted.

The sources

ServiceHookTarget
useRaf()ticked
useScroll(target?)scrolledan element or the window
useWindowScroll()scrolledthe named default
useScrollProgress(target, options?)scrolledInViewan element
useResize(target?)resizedan element
useWindowSize()resizedthe named default
usePointer(target?)movedan element, or the viewport
useDrag(target, options?)draggedan HTMLElement or SVGElement
useKey(target?)keyedthe document, an element or window
useInView(target, init?)intersectedan element
useMutation(target, init?)mutatedany node
useBreakpoint()
useMediaQuery(query)
usePrefersReducedMotion()the named case

LoadService is not ported.

The mixins

withRaf, withScroll, withResize, withScrollProgress, withPointer, withDrag, withInView, withMutation and withKey bind one subscription per mount cycle under the one method name the service owns.

The combinators

Writing your own

The service surface

ts
interface Service<T, R = void> {
  subscribe(callback: (props: T) => R, options?: { immediate?: boolean }): () => void;
  props(): T;
}

subscribe() returns the unsubscribe function. Lazy and reference-counted: the source starts on the first subscriber and stops on the last, so with no subscriber there is no listener, no observer and no frame.

Every prop field is readonly, and the props object belongs to its service. It is valid for the duration of the call that received it — use { ...props } to keep one.

Asking for the first delivery

js
useScroll().subscribe(callback, { immediate: true });

The sources that have a current value honour it. The ones that do not, do nothing, and each service states which it is through hasProps():

Has a current value between deliveriesDoes not
scroll, resize, breakpoint, media query, scroll progressthe frame tick, the pointer before it is seen, a drag outside a gesture, a mutation batch

Only the new subscriber is called, and the first props of a run carry no movement.

One instance per target and per options

Keyed in a WeakMap by perTarget(). The options are read by meaning, not by spelling: object keys are sorted at every depth and the keys holding undefined are dropped, while arrays keep their order.

js
// The same service.
useInView(el, { threshold: 0.5, rootMargin: '0px' });
useInView(el, { rootMargin: '0px', threshold: 0.5 });

Only what the platform owns needs a key of its own: useInView() gives its root a weak id, and useMutation() keeps a canonical init for the DOM contract. Nothing groups observers across targets.

Props are flat

One field per axis, and nothing derivable is a field. lastX is x - deltaX; changedX is deltaX !== 0. The grouped objects of v3 — last, delta, max, progress, direction, changed — are removed.

directionX and directionY are -1 | 0 | 1: one signed value that multiplies.

No service owns a loop

The raf service and an active drag inertia subscribe to scheduler.tick(). The scroll service coalesces its events into one read per frame. The resize service is a ResizeObserver. See The scheduler.

Failures

A subscriber that throws is skipped and reported as callback.service-failed. Core dispatches the diagnostic first and calls reportError() only when no listener cancelled the event.

Publishing is re-entrant, so any code that changes state after a publication checks first that the service is still alive.

MIT Licensed