@azlib/react
v0.10.4
Published
Reusable React hooks and visual components for azlib apps. Hook files are named use-{name}.ts. If a hook needs HTTP, dates, cache, or logging, it imports the matching @azlib package — it does not wrap that package as its own hook.
Readme
React
Reusable React hooks and visual components for azlib apps. Hooks import from @azlib/react. Components import from @azlib/react/components. Import @azlib/react/styles.css once for the default dark addon theme (light via data-theme="light").
Each hook lives in a feature folder as use-{name}.ts (for example form/use-form.ts, lazy-resource/use-lazy-resource.ts). Related internals stay in that folder (form/form-control.ts). Do not add files named after other azlib packages (cache.ts, http.ts, temporal.ts, logger.ts) and do not export a hook whose only job is to wrap that package.
If a hook itself needs HTTP, dates, cache, or logging, import @azlib/http-client, @azlib/temporal, @azlib/cache, or @azlib/logger inside that hook. Never add axios, SWR, date-fns, dayjs, winston, or pino. Callers compose the same way: pass createHttpClient().json into useLazyResource, call temporal() in render, and so on.
Capabilities
useLazyResource— keyed client fetch withdata/loading/error/reloadcreateDataSource/createArrayDataSource/createJsonDataSource/useDataSource— source-agnostic list binding (array, JSON URL, server, or customload) for DataGrid and future Select/List/TreeViewuseObserver— IntersectionObserver, ResizeObserver, or MutationObserveruseEventListener— window/document/element events without stale handlersuseDebouncedValue— delayed value for search and other high-frequency inputuseMediaQuery— CSS media query subscriptionuseForm— headless form values, validation, and submit (register,handleSubmit,formState)useController— bind custom inputs touseFormcontroluseFieldArray— append / remove / reorder repeatable field rows@azlib/react/components— Button, Input, Select, Modal, DataGrid, DatePicker, and the rest of the design-system surface@azlib/react/styles.css— compiled Tailwind v4 default theme (azprefix)
AI Agent Quick Reference
Core exports
| Export | Type | Description |
| --------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------ |
| useLazyResource<T>(key, load) | Hook | Fetches when key is set. Returns { data, error, loading, reload }. |
| createDataSource / createArrayDataSource / createJsonDataSource | Factory | Build a DataSource from custom load, in-memory array, or JSON URL. |
| useDataSource(dataSource, options?) | Hook | { data, totalCount, loading, error, reload, load } for any DataSource. |
| useObserver(options?) | Hook | Observes a node via options.target or the returned ref. Default type is intersection. |
| useEventListener(eventName, handler, target?, options?) | Hook | Listens on window unless target is passed. |
| useDebouncedValue<T>(value, delayMs) | Hook | Returns value after it has been stable for delayMs. |
| useMediaQuery(query) | Hook | true when the CSS media query matches. false during SSR. |
| useForm<T>(options?) | Hook | Form state. Returns { register, handleSubmit, control, formState, watch, setValue, reset, trigger }. |
| useController({ control, name, rules? }) | Hook | { field, fieldState } for custom inputs. field.onChange accepts an event or a raw value. |
| useFieldArray({ control, name }) | Hook | { fields, append, prepend, insert, remove, swap, move, update } with stable ids. |
| LazyResourceKey | Type | string \| number \| bigint \| false \| null \| undefined |
| UseLazyResourceResult<T> | Type | { data?: T; error?: Error; loading: boolean; reload(): Promise<void> } |
| @azlib/react/components | Components | Visual design-system components (Button, DataGrid, ComponentsProvider, …). |
| @azlib/react/styles.css | Stylesheet | Default theme. Import once at the app root. |
Usage
import { createHttpClient } from "@azlib/http-client";
import { useDebouncedValue, useLazyResource, useObserver } from "@azlib/react";
const http = createHttpClient({ timeoutMs: 5_000 });
const query = useDebouncedValue(searchText, 300);
const {
data: profile,
error,
loading,
reload,
} = useLazyResource(
tab === "about" || tab === "feedback" ? itemId : null,
(id) => http.json(`/api/browse/items/${id}`),
);
const { data: feedback } = useLazyResource(
tab === "feedback" && profile?.sellerUserId ? profile.sellerUserId : null,
(userId) => http.json(`/api/browse/feedback?user-id=${userId}`),
);
const { ref, isIntersecting } = useObserver({ rootMargin: "80px" });Pass null, undefined, or false to useLazyResource to skip. The last successful data stays in memory so a tab switch does not blank the UI.
useObserver without target returns a callback ref to attach to the element. Set type: "resize" or type: "mutation" for the other browser observers.
DataSource
import {
createArrayDataSource,
createDataSource,
createJsonDataSource,
useDataSource,
} from "@azlib/react";
import { DataGrid } from "@azlib/react/components";
// In-memory
const priorities = createArrayDataSource({
key: "value",
data: [{ value: "high", label: "High" }],
});
// JSON URL (inject fetchJson to use @azlib/http-client)
const authors = createJsonDataSource({
key: "id",
url: "/api/authors.json",
});
// Server-processed paging
const products = createDataSource({
key: "id",
loadMode: "processed",
load: (opts) => http.json(`/api/products?skip=${opts.skip}&take=${opts.take}`),
});
// Bind without controlled rows
<DataGrid columns={columns} dataSource={products} />
// Lookup column
<DataGrid
rows={books}
columns={[{
key: "authorId",
label: "Author",
dataType: "select",
lookup: { dataSource: authors, valueExpr: "id", displayExpr: "name" },
}]}
/>Forms
import { useController, useFieldArray, useForm } from "@azlib/react";
function ProfileForm() {
const { register, handleSubmit, control, formState } = useForm({
defaultValues: { email: "", title: "", tasks: [{ title: "First" }] },
mode: "onBlur",
});
const { field } = useController({
control,
name: "title",
rules: { required: true },
});
const { fields, append, remove } = useFieldArray({ control, name: "tasks" });
return (
<form onSubmit={handleSubmit((values) => console.log(values))}>
<input
{...register("email", { required: "Email is required", email: true })}
/>
{formState.errors.email && <span>{formState.errors.email.message}</span>}
<input value={String(field.value ?? "")} onChange={field.onChange} />
{fields.map((row, index) => (
<div key={row.id}>
<input {...register(`tasks.${index}.title`)} />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ title: "" })}>
Add task
</button>
<button type="submit">Save</button>
</form>
);
}Validation rules (required, email, url, uuid, min / max, pattern, validate, …) run through @azlib/validator. Schema-driven <EditForm> stays in @azlib/form-engine.
Components
import { Button, ComponentsProvider } from "@azlib/react/components";
import "@azlib/react/styles.css";
function App() {
return (
<ComponentsProvider locale="en">
<Button variant="primary">Save</Button>
</ComponentsProvider>
);
}Behavioral gotchas
useLazyResourcecache is per hook instance, not global. Two components with the same key each fetch once for themselves.loadis read from a ref. Inline arrow functions are fine; the effect depends onkey, notloadidentity. Put@azlib/http-clientcalls inload, not a separate HTTP hook.- Errors are
Errorobjects. Map to a string in the UI witherror?.message. - Not for infinite scroll. Offset pagination still needs local list state;
useLazyResourcecovers one keyed resource at a time. - New hooks go in
{feature}/use-{name}.ts, are exported from{feature}/index.ts, and re-exported from the packageindex.ts. If they need dates or HTTP, import@azlib/temporal/@azlib/http-clientin that file. useFormis headless. Native inputs useregister; custom widgets useuseController. Do not addreact-hook-form.defaultValuesare captured on first mount. Callreset(next)to load a different record.<EditForm>stays in@azlib/form-engine. ImportuseFormfrom@azlib/reactin app code.
