Skip to main content
jg-packages

Search

    React hooks

    jg-react

    React hooks for online status, page visibility, Tailwind CSS breakpoints, and scroll thresholds, each safe in a server render.

    v0.3.0BrowserESMMIT

    npm install jg-react

    Usage

    ts
    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:

    ts
    import { createFileUploadAcceptValue, useFileUploadQueue } from 'jg-react';
    
    const queue = useFileUploadQueue({
      acceptedFileTypes: ['image/png', 'image/jpeg', '.heic'],
      maxFileBytes: 10 * 1024 * 1024,
      maxFiles: 4,
      isPreviewEnabled: true,
      uploadFile: (file, { signal, reportProgress, uploadId }) =>
        uploadToStorage(file, { signal, onProgress: reportProgress, idempotencyKey: uploadId }),
    });
    // queue.addFiles(input.files), queue.items, queue.summary, queue.announcement,
    // queue.cancelUpload(id), queue.retryUpload(id), queue.removeFile(id), queue.reset()
    const accept = createFileUploadAcceptValue(queue.policy.acceptedFileTypes);
    • 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 key
    const 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.

    Functions

    createFileUploadAcceptValue

    function

    Builds a file input's accept attribute from a policy's accepted types.

    Signature
    createFileUploadAcceptValue(
      acceptedFileTypes: readonly string[],
    ): string | undefined

    Parameters

    acceptedFileTypesreadonly string[]

    The policy's accepted types.

    Returns

    string | undefined

    The attribute's value, or undefined when every type is accepted.

    Example
    import { createFileUploadAcceptValue } from 'jg-react';
    
    createFileUploadAcceptValue(['image/png', '.jpg']); // 'image/png,.jpg'

    createThemeInitScript

    function

    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.

    Signature
    createThemeInitScript(options: IThemeInitScriptOptions): string

    Parameters

    optionsIThemeInitScriptOptions

    The localStorage key, and optionally the field and the default preference.

    Returns

    string

    The script's source.

    Throws

    • INVALID_THEME_SCRIPT_OPTIONSThe theme script needs a non-empty storage key, a non-empty field when one is given, and a valid default preference.
    Example
    import { createThemeInitScript } from 'jg-react';
    
    const script = createThemeInitScript({ storageKey: 'jg-storage:preferences', field: 'theme' });

    hasDraggedFiles

    function

    Checks whether a drag carries files, so a drop zone can react to it.

    Signature
    hasDraggedFiles(dragTypes: readonly string[] | DOMStringList): boolean

    Parameters

    dragTypesreadonly string[] | DOMStringList

    The drag's types, such as event.dataTransfer.types.

    Returns

    boolean

    Whether it carries files.

    Example
    import { hasDraggedFiles } from 'jg-react';
    
    const onDragOver = (event: DragEvent) => {
      if (event.dataTransfer !== null && hasDraggedFiles(event.dataTransfer.types)) {
        event.preventDefault();
      }
    };

    isBreakpointAtLeast

    function

    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.

    Signature
    isBreakpointAtLeast(
      breakpoint: IBreakpoint | undefined,
      minimum: IBreakpoint,
    ): boolean

    Parameters

    breakpointIBreakpoint | undefined

    The current breakpoint, from useBreakpoint.

    minimumIBreakpoint

    The narrowest breakpoint that counts.

    Returns

    boolean

    Whether the breakpoint reaches the minimum.

    Example
    import { isBreakpointAtLeast } from 'jg-react';
    
    isBreakpointAtLeast('lg', 'md'); // => true
    isBreakpointAtLeast('xs', 'sm'); // => false
    isBreakpointAtLeast(undefined, 'xs'); // => false

    isFileUploadItemCancelable

    function

    Checks whether an item can still be canceled: it's validating, queued, or sending its bytes. Once every byte is sent, it's processing, and it can't.

    Signature
    isFileUploadItemCancelable<TResult>(item: IFileUploadItem<TResult>): boolean

    Parameters

    itemIFileUploadItem<TResult>

    The item.

    Returns

    boolean

    Whether it can be canceled.

    Example
    import { isFileUploadItemCancelable } from 'jg-react';
    
    declare const item: Parameters<typeof isFileUploadItemCancelable>[0];
    
    const canCancel = isFileUploadItemCancelable(item);

    resolveFileUploadPolicy

    function

    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.

    Signature
    resolveFileUploadPolicy(options: IFileUploadPolicyOptions): IFileUploadPolicy

    Parameters

    optionsIFileUploadPolicyOptions

    The policy's fields.

    Returns

    IFileUploadPolicy

    The policy.

    Throws

    • INVALID_FILE_UPLOAD_POLICYThe accepted file types must be an array.
    • INVALID_FILE_UPLOAD_POLICYEach accepted file type must be a string.
    • INVALID_FILE_UPLOAD_POLICYEach accepted file type must be a file extension, such as .png, or a MIME type, such as image/png.
    • INVALID_FILE_UPLOAD_POLICYThe maximum file size must be a positive integer of bytes.
    • INVALID_FILE_UPLOAD_POLICYThe maximum file count must be a positive integer.
    • INVALID_FILE_UPLOAD_POLICYThe maximum concurrent uploads must be a positive integer.
    • INVALID_FILE_UPLOAD_POLICYThe maximum total size must be a positive integer of bytes, when given.
    Example
    import { resolveFileUploadPolicy } from 'jg-react';
    
    resolveFileUploadPolicy({ acceptedFileTypes: [' IMAGE/PNG '], maxFileBytes: 1_048_576 });
    // { acceptedFileTypes: ['image/png'], maxFiles: 1, maxConcurrentUploads: 1, ... }

    resolveThemePreference

    function

    Resolves a preference to the theme a page shows: the system's color scheme for system.

    Signature
    resolveThemePreference(
      preference: "light" | "dark" | "system",
      systemPrefersDark: boolean,
    ): IResolvedTheme

    Parameters

    preference"light" | "dark" | "system"

    The preference.

    systemPrefersDarkboolean

    Whether the system uses a dark color scheme.

    Returns

    IResolvedTheme

    light or dark.

    Example
    import { resolveThemePreference } from 'jg-react';
    
    resolveThemePreference('system', true); // 'dark'
    resolveThemePreference('light', true); // 'light'

    useAppliedTheme

    function

    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.

    Signature
    useAppliedTheme(
      preference: "light" | "dark" | "system",
      options?: IAppliedThemeOptions,
    ): IResolvedTheme

    Parameters

    preference"light" | "dark" | "system"

    The user's preference, such as from a settings store.

    options?IAppliedThemeOptions

    Whether to turn transitions off while the class changes. Defaults to true.

    Defaults to {}.

    Returns

    IResolvedTheme

    The theme applied, light or dark.

    Example
    import { useAppliedTheme } from 'jg-react';
    
    export const ThemeRoot = ({ preference }: { preference: 'light' | 'dark' | 'system' }) => {
      useAppliedTheme(preference);
      return null;
    };

    useBreakpoint

    function

    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.

    Signature
    useBreakpoint(): IBreakpoint | undefined

    Returns

    IBreakpoint | undefined

    The breakpoint, or undefined in a server render.

    Example
    import { isBreakpointAtLeast, useBreakpoint } from 'jg-react';
    
    export const useIsDesktopLayout = (): boolean => isBreakpointAtLeast(useBreakpoint(), 'lg');

    useCopyFeedback

    function

    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.

    Signature
    useCopyFeedback(options?: IUseCopyFeedbackOptions): IUseCopyFeedbackResult

    Parameters

    options?IUseCopyFeedbackOptions

    How long the copied state lasts, and what receives a failed copy's error.

    Defaults to {}.

    Returns

    IUseCopyFeedbackResult

    Whether a copy just succeeded, and copy, which keeps its identity.

    Throws

    • INVALID_COPY_FEEDBACK_DURATIONThe copy feedback duration must be a positive, finite number of seconds.
    Example
    import { useCopyFeedback } from 'jg-react';
    
    export const useCopyLink = (url: string) => {
      const { isCopied, copy } = useCopyFeedback();
      return { isCopied, copyLink: () => copy(() => navigator.clipboard.writeText(url)) };
    };

    useDocumentVisibility

    function

    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.

    Signature
    useDocumentVisibility(): boolean

    Returns

    boolean

    Whether the page is visible.

    Example
    import { useEffect } from 'react';
    import { useDocumentVisibility } from 'jg-react';
    
    declare const refreshPrices: () => void;
    
    export const usePricePolling = (): void => {
      const isVisible = useDocumentVisibility();
      useEffect(() => {
        if (!isVisible) {
          return undefined;
        }
        const timer = setInterval(refreshPrices, 30_000);
        return () => clearInterval(timer);
      }, [isVisible]);
    };

    useFileUploadQueue

    function

    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.

    Signature
    useFileUploadQueue<TResult>(
      options: IUseFileUploadQueueOptions<TResult>,
    ): IUseFileUploadQueueResult<TResult>

    Parameters

    optionsIUseFileUploadQueueOptions<TResult>

    The policy, the upload and validation functions, the messages, and the callbacks.

    Returns

    IUseFileUploadQueueResult<TResult>

    The controls, which keep their identity, the items, their summary, the latest announcement for a polite live region, and the policy.

    Throws

    • INVALID_FILE_UPLOAD_POLICYThe accepted file types must be an array.
    • INVALID_FILE_UPLOAD_POLICYEach accepted file type must be a string.
    • INVALID_FILE_UPLOAD_POLICYEach accepted file type must be a file extension, such as .png, or a MIME type, such as image/png.
    • INVALID_FILE_UPLOAD_POLICYThe maximum file size must be a positive integer of bytes.
    • INVALID_FILE_UPLOAD_POLICYThe maximum file count must be a positive integer.
    • INVALID_FILE_UPLOAD_POLICYThe maximum concurrent uploads must be a positive integer.
    • INVALID_FILE_UPLOAD_POLICYThe maximum total size must be a positive integer of bytes, when given.
    Example
    import { useFileUploadQueue } from 'jg-react';
    
    declare const uploadAvatar: (file: File, signal: AbortSignal) => Promise<{ url: string }>;
    
    export const useAvatarUpload = () =>
      useFileUploadQueue({
        acceptedFileTypes: ['image/png', 'image/jpeg'],
        maxFileBytes: 5 * 1024 * 1024,
        isPreviewEnabled: true,
        uploadFile: (file, { signal }) => uploadAvatar(file, signal),
      });

    useFormChanges

    function

    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.

    Signature
    useFormChanges<TCommand extends object, TNormalizedCommand extends object = TCommand>(
      options: IUseFormChangesOptions<TCommand, TNormalizedCommand>,
    ): IUseFormChangesResult<TCommand>

    Parameters

    optionsIUseFormChangesOptions<TCommand, TNormalizedCommand>

    The confirmed command, the current one, and the optional normalizer.

    Returns

    IUseFormChangesResult<TCommand>

    Whether the form holds changes, the submit-time check, and confirmChanges.

    Throws

    • UNABLE_TO_SERIALIZE_JSONif a command holds a value JSON can't represent, such as a BigInt
    Example
    import { useFormChanges } from 'jg-react';
    
    type IProfileCommand = { name: string; bio: string };
    
    const trimProfile = ({ name, bio }: IProfileCommand): IProfileCommand => ({
      name: name.trim(),
      bio: bio.trim(),
    });
    
    export const useProfileForm = (saved: IProfileCommand, draft: IProfileCommand) => {
      const { hasChanges, guardSubmission, confirmChanges } = useFormChanges({
        confirmedCommand: saved,
        currentCommand: draft,
        normalizeCommand: trimProfile,
      });
      return { canSave: hasChanges, guardSubmission, confirmChanges };
    };

    useIsWindowScrolledPast

    function

    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.

    Returns

    boolean

    Whether it's scrolled further than that.

    Example
    import { useIsWindowScrolledPast } from 'jg-react';
    
    export const useHeaderShadow = (): string =>
      useIsWindowScrolledPast(8) ? 'shadow-md' : 'shadow-none';

    useOnlineStatus

    function

    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.';
    };

    useResendCooldown

    function

    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.

    Signature
    useResendCooldown(options: IUseResendCooldownOptions): IUseResendCooldownResult

    Parameters

    optionsIUseResendCooldownOptions

    The duration in seconds, and optionally the cooldown's key, the server's last send, and the jg-storage store id.

    Returns

    IUseResendCooldownResult

    Whether it's cooling down, the seconds left, and startCooldown.

    Throws

    • INVALID_COOLDOWN_DURATIONThe cooldown duration must be a positive safe integer of seconds.
    • INVALID_STORE_IDThe store id must hold only letters, digits, underscores, and hyphens.
    Example
    import { useResendCooldown } from 'jg-react';
    
    export const useVerificationResend = (accountId: string, lastSentAt: string | null) =>
      useResendCooldown({
        durationSeconds: 60,
        cooldownKey: `${accountId}:verify-email`,
        lastAttemptedAt: lastSentAt,
      });

    useResolvedTheme

    function

    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.

    Signature
    useResolvedTheme(preference: "light" | "dark" | "system"): IResolvedTheme

    Parameters

    preference"light" | "dark" | "system"

    The user's preference.

    Returns

    IResolvedTheme

    light or dark.

    Example
    import { useResolvedTheme } from 'jg-react';
    
    export const CodeBlock = ({ preference }: { preference: 'light' | 'dark' | 'system' }) => {
      const theme = useResolvedTheme(preference);
      return theme === 'dark' ? 'github-dark' : 'github-light';
    };

    validateFileUploadCandidate

    function

    Checks a file against a policy and the files already admitted, in a fixed order: empty, type, size, duplicate, count, and total size.

    Signature
    validateFileUploadCandidate(
      file: File,
      admittedFiles: readonly File[],
      policy: IFileUploadPolicy,
      messages?: IFileUploadMessages,
    ): IFileUploadValidationIssue | null

    Parameters

    fileFile

    The file.

    admittedFilesreadonly File[]

    The files that count toward the limits.

    policyIFileUploadPolicy

    The policy, from resolveFileUploadPolicy.

    messages?IFileUploadMessages

    The text of the issues. Defaults to DEFAULT_FILE_UPLOAD_MESSAGES.

    Defaults to DEFAULT_FILE_UPLOAD_MESSAGES.

    Returns

    IFileUploadValidationIssue | null

    The first issue, or null when the file is admitted.

    Example
    import { resolveFileUploadPolicy, validateFileUploadCandidate } from 'jg-react';
    
    const policy = resolveFileUploadPolicy({ acceptedFileTypes: ['image/*'], maxFileBytes: 1024 });
    validateFileUploadCandidate(new File([], 'empty.png', { type: 'image/png' }), [], policy);
    // { reason: 'empty-file', message: '"empty.png" is empty and cannot be uploaded.', ... }

    Constants

    DEFAULT_COPY_FEEDBACK_SECONDS

    const

    the seconds the copied state lasts when the options don't set them

    Declaration
    const DEFAULT_COPY_FEEDBACK_SECONDS: 1.4

    DEFAULT_FILE_UPLOAD_MESSAGES

    const

    the queue's text in English, which messages overrides

    Declaration
    const DEFAULT_FILE_UPLOAD_MESSAGES: IFileUploadMessages

    ERRORS

    const

    the codes for every error thrown by the package

    Declaration
    const ERRORS: { INVALID_COOLDOWN_DURATION: "INVALID_COOLDOWN_DURATION"; INVALID_THEME_SCRIPT_OPTIONS: "INVALID_THEME_SCRIPT_OPTIONS"; INVALID_COPY_FEEDBACK_DURATION: "INVALID_COPY_FEEDBACK_DURATION"; INVALID_FILE_UPLOAD_POLICY: "INVALID_FILE_UPLOAD_POLICY" }

    THEME_PREFERENCES

    const

    every theme preference a user can pick

    Declaration
    const THEME_PREFERENCES: readonly ["light", "dark", "system"]

    Types

    IAppliedThemeOptions

    type

    how useAppliedTheme applies the theme

    Declaration
    type IAppliedThemeOptions = {
        disableTransitionsOnChange?: boolean | undefined;
    };

    Members

    • disableTransitionsOnChangeproperty
      boolean

      turns transitions off while the theme class changes, so colors switch at once. Defaults to true

    IBreakpoint

    type

    Tailwind CSS's breakpoints, from the narrowest: xs is below sm, such as a phone

    Declaration
    type IBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';

    IFileUploadCanceledHandler

    type

    receives a file once it's canceled

    Declaration
    type IFileUploadCanceledHandler = (item: IFileUploadCanceledItem) => void;

    IFileUploadCanceledItem

    type

    a file canceled before every byte was sent, which can be retried

    Declaration
    type IFileUploadCanceledItem = IFileUploadItemBase & {
        status: 'canceled';
    };

    IFileUploadControls

    type

    what controls the queue

    Declaration
    type IFileUploadControls = {
        addFiles: (files: Iterable<File>) => void;
        cancelAllUploads: () => void;
        cancelUpload: (uploadId: string) => void;
        removeFile: (uploadId: string) => void;
        reset: () => void;
        retryUpload: (uploadId: string) => void;
        startUploads: () => void;
    };

    Members

    • addFilesproperty
      (files: Iterable<File>) => void

      validates the files and queues the ones admitted, one selection at a time

    • cancelAllUploadsproperty
      () => void

      cancels every file that hasn't finished sending its bytes

    • cancelUploadproperty
      (uploadId: string) => void

      cancels a file that hasn't finished sending its bytes

    • removeFileproperty
      (uploadId: string) => void

      removes a file, canceling it first; a processing file stays

    • resetproperty
      () => void

      removes every file but the processing ones, canceling them first

    • retryUploadproperty
      (uploadId: string) => void

      queues a failed or canceled file again, under the same id

    • startUploadsproperty
      () => void

      starts the queued files, without autoUpload

    IFileUploadErrorDetails

    type

    an error's feedback, with the original error as its cause

    Declaration
    type IFileUploadErrorDetails = IFileUploadErrorFeedback & {
        cause: unknown;
    };

    IFileUploadErrorFeedback

    type

    what the user sees about an error, as getErrorFeedback gives it

    Declaration
    type IFileUploadErrorFeedback = {
        message: string;
        code: string | null;
    };

    Members

    • messageproperty
      string

      the message to show

    • codeproperty
      string | null

      a code the user may quote to support, or null

    IFileUploadFailedItem

    type

    a file whose upload failed, with what to show for its error

    Declaration
    type IFileUploadFailedItem = IFileUploadItemBase & {
        status: 'failed';
        error: IFileUploadErrorDetails;
    };

    IFileUploadFailureHandler

    type

    receives a file once its upload failed

    Declaration
    type IFileUploadFailureHandler = (item: IFileUploadFailedItem) => void;

    IFileUploadHandler

    type

    uploads one file, resolving to what the app keeps, such as the stored file's URL

    Declaration
    type IFileUploadHandler<TResult> = (file: File, context: IFileUploadHandlerContext) => Promise<TResult>;

    IFileUploadHandlerContext

    type

    what an upload attempt receives besides the file

    Declaration
    type IFileUploadHandlerContext = {
        attemptNumber: number;
        reportProgress: (uploadedBytes: number, totalBytes: number) => void;
        signal: AbortSignal;
        uploadId: string;
    };

    Members

    • attemptNumberproperty
      number

      the attempt's number, from 1

    • reportProgressproperty
      (uploadedBytes: number, totalBytes: number) => void

      reports the bytes sent so far; reports are coalesced per frame and never go backwards

    • signalproperty
      AbortSignal

      aborts when the upload is canceled, removed, replaced, or the hook unmounts

    • uploadIdproperty
      string

      the item's id, the same for every attempt, as an idempotency key

    IFileUploadItem

    type

    a file in the queue

    Declaration
    type IFileUploadItem<TResult> = IFileUploadValidatingItem | IFileUploadQueuedItem | IFileUploadUploadingItem | IFileUploadProcessingItem | IFileUploadSucceededItem<TResult> | IFileUploadFailedItem | IFileUploadCanceledItem | IFileUploadRejectedItem;

    IFileUploadItemsChangeHandler

    type

    receives the items and their summary each time the queue changes

    Declaration
    type IFileUploadItemsChangeHandler<TResult> = (items: readonly IFileUploadItem<TResult>[], summary: IFileUploadSummary) => void;

    IFileUploadMessages

    type

    the text the queue shows and announces, which an app can translate or reword

    Declaration
    type IFileUploadMessages = {
        emptyFile: (fileName: string) => string;
        unsupportedFileType: (fileName: string, acceptedFileTypes: readonly string[]) => string;
        fileTooLarge: (fileName: string, maxFileBytes: number, excessBytes: number) => string;
        duplicateFile: (fileName: string) => string;
        fileCountExceeded: (maxFiles: number) => string;
        totalSizeExceeded: (fileName: string, maxTotalBytes: number) => string;
        uploadFailed: string;
        fileIssue: (fileName: string, message: string) => string;
        queued: (fileName: string) => string;
        uploadStarted: (fileName: string) => string;
        uploadComplete: (fileName: string) => string;
        canceled: (fileName: string) => string;
        queuedForRetry: (fileName: string) => string;
        queueCleared: string;
        queueClearedWhileProcessing: string;
    };

    Members

    • emptyFileproperty
      (fileName: string) => string
    • unsupportedFileTypeproperty
      (fileName: string, acceptedFileTypes: readonly string[]) => string
    • fileTooLargeproperty
      (fileName: string, maxFileBytes: number, excessBytes: number) => string
    • duplicateFileproperty
      (fileName: string) => string
    • fileCountExceededproperty
      (maxFiles: number) => string
    • totalSizeExceededproperty
      (fileName: string, maxTotalBytes: number) => string
    • uploadFailedproperty
      string

      the message of a failed upload, or of a validator that threw, unless getErrorFeedback gives one

    • fileIssueproperty
      (fileName: string, message: string) => string

      announcements, for a polite live region

    • queuedproperty
      (fileName: string) => string
    • uploadStartedproperty
      (fileName: string) => string
    • uploadCompleteproperty
      (fileName: string) => string
    • canceledproperty
      (fileName: string) => string
    • queuedForRetryproperty
      (fileName: string) => string
    • queueClearedproperty
      string
    • queueClearedWhileProcessingproperty
      string

    IFileUploadPolicy

    type

    the policy every file is checked against, as resolveFileUploadPolicy normalizes it

    Declaration
    type IFileUploadPolicy = {
        acceptedFileTypes: readonly string[];
        allowDuplicateFiles: boolean;
        autoUpload: boolean;
        maxConcurrentUploads: number;
        maxFileBytes: number;
        maxFiles: number;
        maxTotalBytes: number | null;
        replaceSingleFile: boolean;
    };

    Members

    • acceptedFileTypesproperty
      readonly string[]
    • allowDuplicateFilesproperty
      boolean
    • autoUploadproperty
      boolean
    • maxConcurrentUploadsproperty
      number
    • maxFileBytesproperty
      number
    • maxFilesproperty
      number
    • maxTotalBytesproperty
      number | null
    • replaceSingleFileproperty
      boolean

    IFileUploadPolicyOptions

    type

    what resolveFileUploadPolicy takes

    Declaration
    type IFileUploadPolicyOptions = {
        acceptedFileTypes: readonly string[];
        allowDuplicateFiles?: boolean | undefined;
        autoUpload?: boolean | undefined;
        maxConcurrentUploads?: number | undefined;
        maxFileBytes: number;
        maxFiles?: number | undefined;
        maxTotalBytes?: number | undefined;
        replaceSingleFile?: boolean | undefined;
    };

    Members

    • acceptedFileTypesproperty
      readonly string[]

      file extensions, such as .png, and MIME types, such as image/png or image/*; none accepts every type

    • allowDuplicateFilesproperty
      boolean

      whether a file with the same name, size, type, and date as an admitted one is admitted. Defaults to false

    • autoUploadproperty
      boolean

      whether queued files start uploading at once. Defaults to true

    • maxConcurrentUploadsproperty
      number

      the most uploads running at once. Defaults to 2, and never more than maxFiles

    • maxFileBytesproperty
      number

      the largest file accepted, in bytes

    • maxFilesproperty
      number

      the most files admitted at once, the existing files included. Defaults to 1

    • maxTotalBytesproperty
      number

      the most bytes admitted at once, the existing files included

    • replaceSingleFileproperty
      boolean

      with maxFiles 1, whether a new file replaces the current one, once it validates. Defaults to true

    IFileUploadProcessingItem

    type

    every byte is sent and the upload waits for its result; it can't be canceled anymore

    Declaration
    type IFileUploadProcessingItem = IFileUploadItemBase & {
        status: 'processing';
    };

    IFileUploadProgress

    type

    how much of a file is uploaded; without a report yet, its total is the file's size

    Declaration
    type IFileUploadProgress = {
        hasReportedProgress: boolean;
        percentage: number;
        totalBytes: number;
        uploadedBytes: number;
    };

    Members

    • hasReportedProgressproperty
      boolean
    • percentageproperty
      number
    • totalBytesproperty
      number
    • uploadedBytesproperty
      number

    IFileUploadQueuedItem

    type

    a file admitted, waiting for a slot or for startUploads

    Declaration
    type IFileUploadQueuedItem = IFileUploadItemBase & {
        status: 'queued';
    };

    IFileUploadRejectedHandler

    type

    receives a file the checks refused

    Declaration
    type IFileUploadRejectedHandler = (item: IFileUploadRejectedItem) => void;

    IFileUploadRejectedItem

    type

    a file not admitted, with the check that refused it

    Declaration
    type IFileUploadRejectedItem = IFileUploadItemBase & {
        status: 'rejected';
        validationIssue: IFileUploadValidationIssue;
    };

    IFileUploadStatus

    type

    where a file is in its life in the queue

    Declaration
    type IFileUploadStatus = 'validating' | 'queued' | 'uploading' | 'processing' | 'succeeded' | 'failed' | 'canceled' | 'rejected';

    IFileUploadSucceededItem

    type

    a file uploaded, with what the upload resolved to

    Declaration
    type IFileUploadSucceededItem<TResult> = IFileUploadItemBase & {
        status: 'succeeded';
        result: TResult;
    };

    IFileUploadSuccessHandler

    type

    receives a file once it's uploaded

    Declaration
    type IFileUploadSuccessHandler<TResult> = (item: IFileUploadSucceededItem<TResult>) => void;

    IFileUploadSummary

    type

    the queue's counts by status, and its progress weighted by bytes

    Declaration
    type IFileUploadSummary = {
        activeUploads: number;
        canceledFiles: number;
        failedFiles: number;
        progress: IFileUploadProgress;
        queuedFiles: number;
        rejectedFiles: number;
        succeededFiles: number;
        totalFiles: number;
        validatingFiles: number;
    };

    Members

    • activeUploadsproperty
      number
    • canceledFilesproperty
      number
    • failedFilesproperty
      number
    • progressproperty
      IFileUploadProgress
    • queuedFilesproperty
      number
    • rejectedFilesproperty
      number
    • succeededFilesproperty
      number
    • totalFilesproperty
      number
    • validatingFilesproperty
      number

    IFileUploadUploadingItem

    type

    a file sending its bytes

    Declaration
    type IFileUploadUploadingItem = IFileUploadItemBase & {
        status: 'uploading';
    };

    IFileUploadValidatingItem

    type

    a file being checked

    Declaration
    type IFileUploadValidatingItem = IFileUploadItemBase & {
        status: 'validating';
    };

    IFileUploadValidationIssue

    type

    why a file wasn't admitted, with the message to show

    Declaration
    type IFileUploadValidationIssue = IFileUploadErrorDetails & {
        reason: IFileUploadValidationReason;
    };

    IFileUploadValidationReason

    type

    why a file wasn't admitted: a built-in check, or the app's validator

    Declaration
    type IFileUploadValidationReason = 'custom' | 'duplicate-file' | 'empty-file' | 'file-count-exceeded' | 'file-too-large' | 'total-size-exceeded' | 'unsupported-file-type';

    IFileUploadValidator

    type

    checks a file the built-in checks admitted, such as an image's dimensions, resolving to an issue or null

    Declaration
    type IFileUploadValidator = (file: File, context: IFileUploadValidatorContext) => IFileUploadValidationIssue | null | Promise<IFileUploadValidationIssue | null>;

    IFileUploadValidatorContext

    type

    what the app's validator receives besides the file

    Declaration
    type IFileUploadValidatorContext = {
        admittedFiles: readonly File[];
        signal: AbortSignal;
        uploadId: string;
    };

    Members

    • admittedFilesproperty
      readonly File[]

      the files admitted so far, the existing files included

    • signalproperty
      AbortSignal

      aborts when the file is canceled or removed while it's validated

    • uploadIdproperty
      string

    IFormChangesComparable

    type

    a command a form edits, compared by value as JSON

    Declaration
    type IFormChangesComparable = object;

    IResolvedTheme

    type

    the theme a page shows: light or dark

    Declaration
    type IResolvedTheme = 'light' | 'dark';

    IThemeInitScriptOptions

    type

    what createThemeInitScript reads the stored preference from

    Declaration
    type IThemeInitScriptOptions = {
        storageKey: string;
        field?: string | undefined;
        defaultPreference?: IThemePreference | undefined;
    };

    Members

    • storageKeyproperty
      string

      the localStorage key holding the preference, such as jg-storage:preferences

    • fieldproperty
      string

      the field of a stored object that holds the preference, such as theme

    • defaultPreferenceproperty
      IThemePreference

      the preference used when none is stored or it isn't valid. Defaults to 'system'

    IThemePreference

    type

    the theme a user picks: light, dark, or whatever the system uses

    Declaration
    type IThemePreference = (typeof THEME_PREFERENCES)[number];

    IUseCopyFeedbackOptions

    type

    what useCopyFeedback takes

    Declaration
    type IUseCopyFeedbackOptions = {
        durationSeconds?: number | undefined;
        onError?: ((error: unknown) => void) | undefined;
    };

    Members

    • durationSecondsproperty
      number

      how long isCopied stays true after a copy, in seconds: positive and finite. Defaults to DEFAULT_COPY_FEEDBACK_SECONDS

    • onErrorproperty
      (error: unknown) => void

      receives a failed copy's error, such as to show why it failed. The latest one is called

    IUseCopyFeedbackResult

    type

    what useCopyFeedback returns

    Declaration
    type IUseCopyFeedbackResult = {
        isCopied: boolean;
        copy: (write: () => unknown) => Promise<boolean>;
    };

    Members

    • isCopiedproperty
      boolean

      whether a copy just succeeded, for a moment

    • copyproperty
      (write: () => unknown) => Promise<boolean>

      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

    IUseFileUploadQueueOptions

    type

    what useFileUploadQueue takes

    Declaration
    type IUseFileUploadQueueOptions<TResult> = IFileUploadPolicyOptions & {
        uploadFile: IFileUploadHandler<TResult>;
        validateFile?: IFileUploadValidator | undefined;
        disabled?: boolean | undefined;
        existingFiles?: readonly File[] | undefined;
        isPreviewEnabled?: boolean | undefined;
        messages?: Partial<IFileUploadMessages> | undefined;
        getErrorFeedback?: ((error: unknown) => IFileUploadErrorFeedback) | undefined;
        controlsRef?: Ref<IFileUploadControls> | undefined;
        onItemsChange?: IFileUploadItemsChangeHandler<TResult> | undefined;
        onFileRejected?: IFileUploadRejectedHandler | undefined;
        onUploadSuccess?: IFileUploadSuccessHandler<TResult> | undefined;
        onUploadFailure?: IFileUploadFailureHandler | undefined;
        onUploadCanceled?: IFileUploadCanceledHandler | undefined;
    };

    IUseFileUploadQueueResult

    type

    what useFileUploadQueue returns: its controls, its items and their summary, the latest announcement, and the policy

    Declaration
    type IUseFileUploadQueueResult<TResult> = IFileUploadControls & {
        announcement: string;
        items: readonly IFileUploadItem<TResult>[];
        policy: IFileUploadPolicy;
        summary: IFileUploadSummary;
    };

    IUseFormChangesOptions

    type

    what useFormChanges compares: the command being edited against the last confirmed one

    Declaration
    type IUseFormChangesOptions<TCommand extends IFormChangesComparable, TNormalizedCommand extends IFormChangesComparable = TCommand> = {
        confirmedCommand: TCommand;
        currentCommand: TCommand;
        normalizeCommand?: ((command: TCommand) => TNormalizedCommand) | undefined;
    };

    Members

    • confirmedCommandproperty
      TCommand

      the command as the server last confirmed it, such as the saved profile

    • currentCommandproperty
      TCommand

      the command the form holds now

    • normalizeCommandproperty
      (command: TCommand) => TNormalizedCommand

      turns a command into the form both are compared in, such as with trimmed text

    IUseFormChangesResult

    type

    whether the form holds changes, and what advances its baseline

    Declaration
    type IUseFormChangesResult<TCommand extends IFormChangesComparable> = {
        confirmChanges: (command: TCommand) => void;
        guardSubmission: (command: TCommand) => boolean;
        hasChanges: boolean;
    };

    Members

    • confirmChangesproperty
      (command: TCommand) => void

      makes a command the baseline, once the server confirmed saving it

    • guardSubmissionproperty
      (command: TCommand) => boolean

      checks, as the form submits, whether a command still differs from the baseline

    • hasChangesproperty
      boolean

      whether the current command differs from the baseline

    IUseResendCooldownOptions

    type

    what useResendCooldown takes

    Declaration
    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'

    IUseResendCooldownResult

    type

    the cooldown's state, and what starts it

    Declaration
    type IUseResendCooldownResult = {
        isCoolingDown: boolean;
        remainingSeconds: number;
        startCooldown: () => void;
    };

    Members

    • isCoolingDownproperty
      boolean

      whether a resend must wait

    • remainingSecondsproperty
      number

      the whole seconds left, rounded up, 0 when it needn't wait

    • startCooldownproperty
      () => void

      starts the cooldown, once the server acknowledged a send