Nasaq
Components

Hooks

Small client hooks used across apps; useDebounce and useDebouncedCallback for search and autosave, useMediaQuery and useIsMobile for responsive logic, and useInfiniteScroll for loading the next page near the end of a list.

Code

import { Button, Input, useDebounce, useInfiniteScroll } from "@fadymondy/nasaq/web";import { useState } from "react";declare function useSearch(q: string): { rows: string[]; hasMore: boolean; loading: boolean; loadMore(): void };export function Search() {  const [q, setQ] = useState("");  const debounced = useDebounce(q, 300);  const { rows, hasMore, loading, loadMore } = useSearch(debounced);  const sentinel = useInfiniteScroll({ onLoadMore: loadMore, hasMore, loading });  return (    <>      <Input value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search" />      <ul>{rows.map((r) => <li key={r}>{r}</li>)}</ul>      {hasMore ? <Button ref={sentinel} onClick={loadMore}>Load more</Button> : null}    </>  );}

Utilities · beta

Live examples and controls: Hooks in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/hooks.json

Plain React hooks with no dependencies. They are safe to render on the server.

When to use

  • useDebounce: filter or search after typing stops.
  • useDebouncedCallback: autosave, resize handlers, anything that should run once after a burst.
  • useMediaQuery / useIsMobile: when the logic, not only the CSS, changes with the screen.
  • useInfiniteScroll: feeds and long lists that load page by page.

When not to use

  • Layout changes only: use Tailwind breakpoints in CSS, not useIsMobile.
  • Very long lists: virtualise the rows; infinite scroll alone keeps every row in the DOM.

Import

import { useDebounce, useDebouncedCallback, useMediaQuery, useIsMobile, useInfiniteScroll } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { Button, Input, useDebounce, useInfiniteScroll } from "@fadymondy/nasaq/web";
import { useState } from "react";

declare function useSearch(q: string): { rows: string[]; hasMore: boolean; loading: boolean; loadMore(): void };

export function Search() {
  const [q, setQ] = useState("");
  const debounced = useDebounce(q, 300);
  const { rows, hasMore, loading, loadMore } = useSearch(debounced);
  const sentinel = useInfiniteScroll({ onLoadMore: loadMore, hasMore, loading });
  return (
    <>
      <Input value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search" />
      <ul>{rows.map((r) => <li key={r}>{r}</li>)}</ul>
      {hasMore ? <Button ref={sentinel} onClick={loadMore}>Load more</Button> : null}
    </>
  );
}

API

HookReturnsNotes
useDebounce(value, delay = 300)the valueUpdates delay ms after the last change.
useDebouncedCallback(fn, delay = 300)a stable function with .cancel()Always calls the latest fn. Cancelled on unmount.
useMediaQuery(query)booleanfalse on the server and the first client render.
useIsMobile(breakpoint = 768)booleanTrue below the breakpoint (Tailwind md).
useInfiniteScroll(options)a ref callbackPut it on an element after the last item.

UseInfiniteScrollOptions

OptionTypeDefaultDescription
onLoadMore() => voidrequiredNot called again while loading.
hasMorebooleanrequiredStops observing when false.
loadingbooleanfalse
rootMarginstring"240px"Start loading this far before the sentinel shows.
rootElement | nullthe viewportThe scrolling element, when it is not the page.
disabledbooleanfalse

Accessibility

  • Infinite scroll is invisible to keyboard and screen-reader users: keep a "Load more" button as the sentinel, and announce new rows (aria-live) when it helps.
  • useMediaQuery("(prefers-reduced-motion: reduce)") lets JavaScript animations respect the setting.

RTL & i18n

  • No text or direction.

Styling & tokens

  • No styles.

Do / Don't

  • Do debounce the value you query with, not the input's own value.
  • Don't render different content on the server and client from useIsMobile; it is false on the server.

Lab

https://docs.nasaqui.com/?path=/docs/components-utilities-hooks--docs

On this page