import { isBreakpointAtLeast, useBreakpoint, useDocumentVisibility, useIsWindowScrolledPast, useOnlineStatus,} from 'jg-react';export const useLayoutState = () => { const isOnline = useOnlineStatus(); // the browser's guess, from its network interfaces const isVisible = useDocumentVisibility(); // false while the tab is hidden const breakpoint = useBreakpoint(); // 'xs' on a phone, then 'sm' to '2xl' const isScrolled = useIsWindowScrolledPast(8); return { isOnline, isVisible, isDesktop: isBreakpointAtLeast(breakpoint, 'lg'), isScrolled };};
Hook
Returns
Re-renders when
In a server render
useOnlineStatus()
Whether the browser is online
It goes online or offline
true
useDocumentVisibility()
Whether the page is visible
Its tab is hidden or shown
true
useBreakpoint()
Tailwind CSS’s breakpoint: xs below sm, up to 2xl
The viewport crosses a breakpoint
undefined
useIsWindowScrolledPast(pixels)
Whether the window is scrolled further than pixels
The answer changes, not every scroll
false
useFormChanges(options)
Whether a form’s command differs from the last confirmed one, a submit-time check, and confirmChanges
The commands or the baseline change
Compared as usual
useResendCooldown(options)
Whether a resend must wait, the seconds left, and startCooldown
Every second while cooling down
{ isCoolingDown: false, remainingSeconds: 0 }
useCopyFeedback(options?)
Whether a copy just succeeded, and copy(write)
A copy succeeds, and its feedback ends
isCopied: false
useFileUploadQueue(options)
The upload queue’s items, summary, announcement, and controls
The queue changes
An empty queue
useResolvedTheme(preference)
light or dark, following the system while the preference is system
The system’s color scheme changes, for system
dark for dark, otherwise light
useAppliedTheme(preference, options?)
The theme it sets as the light or dark class on <html>
As useResolvedTheme
As useResolvedTheme, without touching the document
Keep a save button disabled until the form holds a real change, and a resend button waiting after each send:
ts
import { useFormChanges, useResendCooldown } from 'jg-react';const trim = ({ name }: { name: string }) => ({ name: name.trim() });export const useProfileEditor = (saved: { name: string }, draft: { name: string }) => { // a whitespace-only edit isn't a change, and the baseline advances only once a save succeeds const { hasChanges, confirmChanges } = useFormChanges({ confirmedCommand: saved, currentCommand: draft, normalizeCommand: trim, }); // the deadline survives a reload of the tab, apart from other accounts and actions const resend = useResendCooldown({ durationSeconds: 60, cooldownKey: 'account-a:verify-email' }); return { canSave: hasChanges, confirmChanges, resend };};
The browser hooks read the browser through useSyncExternalStore, so every component sees the same value in the same render, and a server render gets a fixed value instead of failing. The breakpoints are Tailwind CSS’s defaults, in rem, read through media queries, so they follow the browser’s font size as CSS does. Prefer CSS for layout, and useBreakpoint for what CSS can’t do, such as rendering a different component.
Copy feedback
useCopyFeedback shows that a copy worked, for a moment. copy takes the function that writes, such as jg-clipboard’s copyText, and calls it at once, before awaiting anything, so the write stays inside the click that browsers require for clipboard access:
ts
import { useCopyFeedback } from 'jg-react';const { isCopied, copy } = useCopyFeedback({ durationSeconds: 1.4, onError: showCopyError });const onClick = () => void copy(() => navigator.clipboard.writeText(inviteLink)); // resolves to whether it worked
isCopied stays true for durationSeconds, DEFAULT_COPY_FEEDBACK_SECONDS (1.4) by default, and a new copy restarts it. A copy while another runs is ignored and resolves to false. A failed one resolves to false and calls the latest onError; copy never rejects. After unmount, a copy that settles changes nothing and calls nothing. A duration that isn’t positive and finite throws INVALID_COPY_FEEDBACK_DURATION.
File uploads
useFileUploadQueue runs a file upload queue without rendering anything, for a drop zone or a file input in the app’s own markup:
Checks: each selection is checked file by file, in a fixed order: empty, type, size, duplicate, count, and total size, with existingFiles counted toward the limits. A validateFile function runs next, such as for an image’s dimensions, and the built-in checks run again after it, since the queue may have changed meanwhile. Selections are checked one at a time.
Uploads: admitted files upload first in, first out, up to maxConcurrentUploads at once, 2 by default, or wait for startUploads without autoUpload. Progress is coalesced per frame and never goes backwards. At 100%, a file is processing: it waits for its result, can’t be canceled, and survives removal and reset.
Cancel and retry: a canceled upload is aborted through its signal, and its late result is ignored, but its slot frees only when its promise settles. A retry keeps the item’s uploadId and starts the next attemptNumber.
One file: with maxFiles 1, the default, a new file replaces the current one only once it passes its checks, so a rejected replacement leaves the current file alone.
Cleanup: preview object URLs are revoked on remove, replace, reset, and unmount, and an error a callback throws goes to reportError without stopping the queue. Shown again after an <Activity> hid it, files whose work stopped are canceled.
Text: every message and announcement comes from DEFAULT_FILE_UPLOAD_MESSAGES, which messages overrides, and getErrorFeedback decides what a failed upload shows, messages.uploadFailed by default.
A policy that isn’t valid, such as a maxFileBytes that isn’t a positive integer, throws INVALID_FILE_UPLOAD_POLICY, with a message for each field. resolveFileUploadPolicy, validateFileUploadCandidate, isFileUploadItemCancelable, and hasDraggedFiles work on their own too.
Light and dark themes
createThemeInitScript builds an inline script that applies the stored preference before the page paints, so a server render never flashes the wrong theme, and useAppliedTheme keeps the class on <html> in step once React runs:
tsx
import { createThemeInitScript, useAppliedTheme } from 'jg-react';// jg-storage's createLocalStore('preferences') keeps { theme } under this keyconst script = createThemeInitScript({ storageKey: 'jg-storage:preferences', field: 'theme' });export const ThemeScript = ({ nonce }: { nonce: string }) => ( <script nonce={nonce} dangerouslySetInnerHTML={{ __html: script }} />);export const ThemeRoot = ({ preference }: { preference: 'light' | 'dark' | 'system' }) => { useAppliedTheme(preference); // turns transitions off while the class changes return null;};
Set color-scheme per class in your stylesheet, such as :root.dark { color-scheme: dark; }, so form controls and scrollbars follow the theme.
Generated from TSDoc
API reference
Every export of [email protected], 65 in all, with its signature, its errors, and an example.
Builds the inline script that applies the stored theme before the page paints, so a server render doesn't flash the wrong theme. It reads the preference from localStorage, as JSON or as a raw string, from a field of a stored object when field is given, falls back to defaultPreference when it's missing or invalid, and sets the light or dark class on <html>. Storage that can't be read and a browser without matchMedia never make it throw.
Render it at the top of <head>, with your CSP nonce, as <script nonce={nonce} dangerouslySetInnerHTML={{ __html: script }} />. Its output is the same for the same options, so a hash-based CSP works too. A jg-storage store's key is jg-storage:<id>. Set color-scheme per class in your stylesheet.
Checks whether a breakpoint is at least as wide as another, such as to show a sidebar from md up. An unknown breakpoint, as in a server render, is never wide enough.
Checks and normalizes an upload policy: accepted types trimmed, lowercased, and without repeats, the defaults filled in, concurrency capped at maxFiles, and single-file replacement only for one file.
Applies a theme preference to the page: the light or dark class on <html>, following the system's color scheme while the preference is system. The class changes before the browser paints, with transitions off by default, so colors switch at once. Pair it with createThemeInitScript, so the page shows the right theme before React loads, and set color-scheme per class in your stylesheet.
Reports the viewport's Tailwind CSS breakpoint, re-rendering only when the viewport crosses one: xs below sm, such as on a phone, then sm, md, lg, xl, and 2xl. A server render, which has no viewport, reports undefined, so prefer CSS for layout and this for what CSS can't do.
Shows that a copy worked, for a moment, such as a "Copied" label on a copy button. copy calls the write function at once, before it awaits anything, so a clipboard write still runs inside the click that browsers require; then isCopied stays true for durationSeconds. A copy while another runs is ignored, and a failed one calls onError and leaves isCopied false. copy never rejects, and after unmount nothing changes and nothing is called.
Reports whether the page is visible, re-rendering when its tab is hidden or shown again, such as to pause polling while nobody looks. A server render reports visible.
Runs a headless file upload queue, for a drop zone or a file input in the app's own markup. Selections are checked one at a time, against the built-in checks, then validateFile, then the built-in checks again, since the queue may have changed meanwhile. Admitted files upload first in, first out, up to maxConcurrentUploads at once, with progress coalesced per frame. At 100%, an upload is processing: it can't be canceled anymore, and it survives removal and reset. A canceled upload is aborted and its late result ignored, but its slot frees only once its promise settles. A retry keeps the item's id. With maxFiles 1, a new file replaces the current one only once it validates. Object URLs are revoked on remove, replace, reset, and unmount, and errors the callbacks throw go to reportError. Shown again after an <Activity> hid it, items whose work stopped are canceled.
Tells whether a form holds unsaved changes, by comparing the command it edits with the last one the server confirmed, so a save button can stay disabled until something changed. Commands are compared by value, as JSON: a Map or a Set compares as {}, a key whose value is undefined doesn't count, and a BigInt throws UNABLE_TO_SERIALIZE_JSON. Pass normalizeCommand to compare commands the way the server stores them, such as with trimmed text.
The baseline advances only when confirmedCommand changes by value, or through confirmChanges once a save succeeds, so a failed save keeps the changes. guardSubmission checks again as the form submits, in case the command changed since the last render.
Reports whether the window is scrolled past a number of pixels, such as to shadow a sticky header or show a back-to-top button. It re-renders only when the answer changes, not on every scroll. A server render reports false.
Signature
useIsWindowScrolledPast(pixels: number): boolean
Parameters
pixelsnumber
How far down the window must be scrolled, in pixels.
Reports whether the browser is online, re-rendering when it goes online or offline. It's the browser's own guess, from its network interfaces, not proof that a server can be reached, so use it to explain a failure, not to skip a request. A server render reports online.
Signature
useOnlineStatus(): boolean
Returns
boolean
Whether the browser is online.
Example
import { useOnlineStatus } from 'jg-react';export const OfflineBanner = () => { const isOnline = useOnlineStatus(); return isOnline ? null : 'You are offline. Changes will sync when you reconnect.';};
Makes a resend button, such as for a verification email or a one-time code, wait after each send, with a countdown. Call startCooldown once the server acknowledged a send. With a cooldownKey, the deadline is kept in sessionStorage, so it survives a reload of the tab, and in memory when storage fails. Until the tab sends itself, lastAttemptedAt, when the server says the last send happened, sets the deadline; a time in the future counts as now.
The first render, on the server and while hydrating, is always { isCoolingDown: false, remainingSeconds: 0 }, and the deadline is read right after mount, before the browser paints. So a server-rendered button looks enabled until hydration, when it can't be clicked anyway.
Resolves a theme preference to the theme a page shows, following the system's color scheme while the preference is system, and re-rendering when it changes. A server render, which can't read the system, gives dark for dark and light otherwise.
calls write at once, inside the click that called copy, and resolves to whether it succeeded. It never rejects, and resolves to false at once while another copy runs
type IUseResendCooldownOptions = { durationSeconds: number; cooldownKey?: string | undefined; lastAttemptedAt?: string | number | Date | null | undefined; storeId?: string | undefined;};
Members
durationSecondsproperty
number
how long a resend waits after a send, in seconds: a positive integer
cooldownKeyproperty
string
what the cooldown is for, such as ${accountId}:verify-email, so it survives a reload of the tab and stays apart from other accounts' and actions'. Without it, nothing is stored
lastAttemptedAtproperty
string | number | Date | null
when the server says the last send happened, used until the tab sends one itself
storeIdproperty
string
the jg-storage session store holding the deadlines: letters, digits, underscores, and hyphens. Defaults to 'resend-cooldowns'