Hooks

The React hooks the components here are built on — controlled/uncontrolled state, reduced motion, viewport, intersection.

Quick Preview

useUncontrolled

3uncontrolled

useIsMobile · useReducedMotion

Wide viewportReduced motion off

Resize the window, or flip the OS motion setting, and both follow.

useIntersection

Scroll down…

Out of view

Usage

Four hooks that were internal to this package, published because a component built on top of these needs the same behaviour and the same answers. Each is its own module, so importing one does not pull in the rest.

import { useUncontrolled } from "@giddaa-housing/ui/utils/use-uncontrolled";
import { useReducedMotion } from "@giddaa-housing/ui/utils/use-reduced-motion";
import { useIsMobile } from "@giddaa-housing/ui/utils/use-is-mobile";
import { useIntersection } from "@giddaa-housing/ui/utils/use-intersection";

Examples

All four, live

useUncontrolled

3uncontrolled

useIsMobile · useReducedMotion

Wide viewportReduced motion off

Resize the window, or flip the OS motion setting, and both follow.

useIntersection

Scroll down…

Out of view

useUncontrolled

The controlled/uncontrolled pattern every component here follows: it works on its own, and hands over the moment a value prop arrives. Returns the value, a setter, and whether it is currently controlled.

function Counter({ value, defaultValue, onChange }) {
  const [count, setCount, controlled] = useUncontrolled({
    value,
    defaultValue,
    finalValue: 0, // used when neither value nor defaultValue is given
    onChange,
  });

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

When controlled, the setter is your onChange and nothing else — no internal state is written, so the component cannot drift from the value you passed.

useReducedMotion

Whether the OS asks for less motion. Read it rather than only writing motion-reduce: classes when the thing to change is behaviour rather than style — this package uses it to make a carousel jump between slides instead of animating, which no CSS override can do.

const reducedMotion = useReducedMotion();

element.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth" });

It is false on the first render and correct after mount, so it will not mismatch during hydration.

useIsMobile

Whether the viewport is under a breakpoint, defaulting to 1024. For layout, prefer a CSS breakpoint; reach for this when the decision is structural — rendering a drawer instead of a sidebar, as Sidebar does, rather than restyling one.

const isMobile = useIsMobile(768);

return isMobile ? <Drawer>{nav}</Drawer> : <Sidebar>{nav}</Sidebar>;

useIntersection

An IntersectionObserver as a ref callback. Returns the latest entry, so the full observer API — isIntersecting, intersectionRatio, boundingClientRect — is available rather than a boolean.

const { ref, entry } = useIntersection<HTMLDivElement>({ threshold: 0.9 });

useEffect(() => {
  if (entry?.isIntersecting) loadMore();
}, [entry]);

return <div ref={ref} />;

Props

Prop/APITypeDefaultDescription
useUncontrolled({ value?, defaultValue?, finalValue?, onChange? }) => [value, setValue, controlled]-Controlled/uncontrolled state. finalValue is the fallback when neither value nor defaultValue is given.
useReducedMotion() => booleanfalse before mountTracks prefers-reduced-motion: reduce.
subscribeToReducedMotion(onChange) => () => void-The subscription behind it, exported so it can be tested or used outside React.
useIsMobile(breakpoint?: number) => boolean1024Whether the viewport is narrower than breakpoint.
DEFAULT_MOBILE_BREAKPOINTnumber1024The default, exported so a layout can match it.
useIntersection(options?: IntersectionObserverInit) => { ref, entry }-Observes the element the ref lands on. entry is null before the first observation.

Accessibility

  • useReducedMotion is the behavioural half of honouring a motion preference. Use it for anything CSS cannot reach — scroll behaviour, autoplay, animated transitions between routes — and motion-reduce: classes for the rest.
  • Do not use useIsMobile to hide content. A narrow viewport is not a smaller need; move or restructure rather than remove.
  • Content revealed by useIntersection should still be reachable without scrolling — a keyboard user tabbing into a not-yet-observed region needs it to exist.

On this page