npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@paragrav/rhf-utils

v0.62.0

Published

Integration utilities for react-hook-form.

Readme

README

About

Integration and utility library for react-hook-form (and zod). Declaratively configure your forms via global and form-level options.

Features

  • TypeScript-first
  • global configuration (🔗 RhfUtilsClientConfig section)
    • RHF (UseFormProps) and utilities (RhfUtilsFormOptions) options defaults
    • inject your own hooks and UI (FormChildren)
    • server error transformation (onSubmitErrorUnknown) ✨
    • handle cancel prompt (useCanFormBeCancelled) ✨
    • RHF FormState.errors logging/throwing (RhfUtilsClientConfig.fieldErrors) ✨
  • form-level configuration (🔗 RhfUtilsZodForm section)
    • RHF and utilities options overrides
      • extendable utilities options type (RhfUtilsFormOptions)
    • throw error (FormSubmitError) in submit handler to add errors to RHF context and fail submit
    • handle submit error (onSubmitError / useLastSubmitError)
    • context injection for children (RhfUtilsZodForm.Children), including:
      • formId, formRef, RHF context, utilities options
      • schema-typed Controller component and FormSubmitError class
  • form state relay (🔗 FormRelayContextProvider section) ✨
    • access specific form's state from outside that form/provider
    • assign forms into groups and watch collectively
  • simpler/flatter FieldErrors structure (🔗 FlatFieldErrors section)
    • find orphans errors (🔗 orphan errors section) ✨
    • context groups errors into all, fields, roots, and orphans (useFlatFieldErrorsContext)
  • safer FieldValues type (🔗 SafeFieldValues section)
  • zod support, including input/output types for transformations
  • ~2.8kB min+gzip core (excluding peer dependencies)

Install

npm install @paragrav/rhf-utils  # npm
pnpm add @paragrav/rhf-utils     # pnpm
yarn add @paragrav/rhf-utils     # yarn

Quick Start

  • 🔗 Config (RhfUtilsClientConfig): define your global config (optional)
    • 🔗 Provider (RhfUtilsClientConfigProvider): add to your global stack
  • 🔗 Form With Providers (RhfUtilsZodFormWithProviders): use as form component
  • 🔗 Form State Relay (FormRelayContextProvider): relay form state

Config

Create a config (RhfUtilsClientConfig) object with desired global config and default options. Config applies across all forms, and defaults can be overridden per form.

export const rhfUtilsClientConfig: RhfUtilsClientConfig = {
  defaults: {
    // globally-relevant subset of RHF's `UseFormProps` options
    rhf: {
      mode: 'onSubmit',
    },

    // RhfUtilsFormOptions (see further below)
    utils: {
      stopSubmitPropagation: true,
      devTool: import.meta.env.DEV && { placement: 'top-right' },
    },

    // global default native form props
    form: {
      noValidate: true,
      className: 'my-form-class',
    },
  },

  // optional form component to use (defaults to primitive HTML form)
  FormComponent: Form.Root,

  // inject your own hooks and components
  // into all RhfUtilsZodForm instances
  FormChildren: (
    // RhfUtilsFormComponentProps
    {
      formId, //          unique id string
      formRef, //         form element ref
      context, //         rhf UseFormReturn (without proxy `formState`)
      options, //         RhfUtilsFormOptions (with any custom props)
      Controller, //      rhf controller (SafeFieldValues-typed; no schema at this level)
      FormSubmitError, // error class (SafeFieldValues-typed; no schema at this level)
      children, //        RhfUtilsZodForm's Children instance
    },
  ) => {
    useMyGlobalFormHook();

    // form-level control of your own hooks/behaviors via custom option props
    // (see "Extend RhfUtilsFormOptions" section for more info)
    useMyOptionalFormHook({
      enabled: !!options.enableMyOptionalFormHook,
    });

    return (
      <>
        {/* RhfUtilsZodForm.Children "outlet" (see "Component Hierarchy" section) */}
        {children}

        {/* root errors list */}
        <RootErrorsList />
      </>
    );
  },

  // hook that returns callback to determine whether form can be cancelled
  // if `true`, `RhfUtilsZodForm.onCancel` at form-level is called (e.g., parent component hides form)
  // this allows you to handle non-navigational blocking/prompting
  // NOTE: this library is router-agnostic, so navigation blocking should happen elsewhere
  // (i.e., calling your own hook in `FormComponent`)
  useCanFormBeCancelled: () => {
    const { isDirty } = useFormState();

    // return callback to be called at event-time
    return ({ options }) =>
      !options.enableMyPrompter || // my prompter option not enabled
      !isDirty || // or rhf form state not dirty
      confirm('Are you sure you want to cancel?'); // or user confirms
  },

  // non-FormSubmitError thrown in onSubmit
  // use case: handle and transform server error data for frontend
  onSubmitErrorUnknown: (
    error, // unknown
  ) => {
    // return FormSubmitFieldErrors object to be merged to RHF context errors
    if (isMyServerError(error))
      return transformMyServerErrorToFormSubmitFieldErrors(error);
  },

  // RHF FormState.errors output for debugging
  fieldErrors: {
    // callbacks to determine when to output information about field errors
    // provided `FlatFieldErrorsContext` (all, fields, roots, orphans, hasOrphans)
    // (See "Orphan Errors" section below for more info.)
    output: {
      // determine when to console ("debug" or "error")
      console: ({ hasOrphans }) =>
        // facilitate local debugging of form validation
        (import.meta.env.DEV && { type: 'debug' }) ||
        // console error for reporting on prod
        (hasOrphans && { type: 'error' }),

      // callback to determine when to throw an error based on field errors
      throw: ({ hasOrphans }) =>
        // bring attention to orphans locally
        import.meta.env.DEV && hasOrphans,
    },
  },
};

Config Provider

Add the context provider to your global provider stack.

<RhfUtilsClientConfigProvider config={rhfUtilsClientConfig}>
  {children}
</RhfUtilsClientConfigProvider>

Form With Providers

Currently, only zod schemas are supported.

<RhfUtilsZodFormWithProviders
  schema={z.object({
    email: z.string().min(1).email(),
    password: z.string().min(1),
    passwordConfirm: z.string().min(1),
  })}
  defaultValues={{ email: '', password: '', passwordConfirm: '' }}
  getApiData={({ email, password }) => ({ email, password })}
  // cancel handler (passed to `Children` below)
  // execution routed through `RhfUtilsClientConfig.useCanFormBeCancelled`, if provided
  onCancel={handleCancel}
  // SUBMIT HANDLERS
  // invariants
  onBeforeSubmitInvariants={async (
    { input, output, api }, // schema input (form values), output; api data (via getApiData)
    context, //                UseRhfUtilsFormOnSubmitContext (id, ref, rhf context (without proxy formState), utils options, schema-typed FormSubmitError class)
    event, //                  SubmitEvent
  ) => [
    {
      field: 'passwordConfirm', // type safe
      message: 'Passwords must match.',
      valid: data.password === data.passwordConfirm,
    },
  ]}
  onBeforeSubmit={({ input, output, api }, context, event) => {
    if (!isValid(input.email))
      throw new context.FormSubmitError({
        email: { message: 'Email is invalid.' },
      });
  }}
  // submit handler
  onSubmit={({ input, output, api }, context, event) => registerService(api)}
  onSubmitSuccess={(onSubmitResult, { input, output, api }, context, event) =>
    navigate('/app/' + onSubmitResult.id)
  }
  // handle error declaratively (i.e., no throw/catch)
  onSubmitError={({ error, context, event }) => {
    handleError(error);
  }}
  onSubmitFinally={() => {
    setState();
  }}
  // form-specific options/overrides (merged with defaults)
  rhf={{
    mode: 'onBlur',
  }}
  utils={{
    submitOnWatch: { debounce: 250 },
    // custom option props
    // (see "Extend RhfUtilsFormOptions" section)
    enableMyOptionalFormHook: true,
  }}
  // surfaced form element attributes
  id="my-form-id"
  className="my-special-form-class" // merged with global defaults
  // other form element attributes
  form={{ noValidate: true }} // merged with global defaults
  // fields
  Children={({
    // UseRhfUtilsFormChildrenProps
    formId, //          unique id string (auto-gen if not supplied)
    formRef, //         ref
    context, //         rhf UseFormReturn (without proxy `formState`)
    options, //         RhfUtilsFormOptions
    Controller, //      schema-typed rhf controller
    FormSubmitError, // schema-typed error class
    onCancel, //        wrapped cancel handler from above
  }) => (
    <>
      <Controller
        name="email" // schema-typed field name
        render={({ field, formState: { isSubmitting } }) => (
          <Stack>
            <label>
              Email
              <input {...field} disabled={isSubmitting} />
            </label>

            <ErrorMessage path={field.name} />
          </Stack>
        )}
      />

      <button type="button" onClick={onCancel}>
        Cancel
      </button>
      <button type="submit">Login</button>
    </>
  )}
/>

Form Children

Children prop takes a component which is rendered as a child of the form (and, if supplied, of RhfUtilsClientConfig.FormComponent). It receives UseRhfUtilsFormChildrenProps as props, including a type-safe Controller component.

If you prefer to define your Children component as standalone, you will need to specify any dependencies as props (e.g., onEvent callback):

const Children: RhfUtilsUseZodFormChildrenFC<
  typeof schema,
  { onEvent: () => void } // other props (if required)
> = ({ ... }) => {};

// or

function Children({ ... }: RhfUtilsUseZodFormChildrenProps<
  typeof schema,
  { onEvent: () => void } // other props (if required)
>) {}

And pass other props manually, if applicable:

<RhfUtilsZodFormWithProviders
  Children={(rhfUtilsZodFormChildrenProps) => (
    <Children
      {...rhfUtilsZodFormChildrenProps}
      props={{ onEvent: handleEvent }}
    />
  )}
/>

Form Component Hierarchy

For each form instance, RhfUtilsZodForm renders as the following:

<ReactHookForm.FormProvider>             // standard rhf provider
  <RhfUtilsProviders>                    // rhf utils internal providers
    <RhfUtilsClientConfig.FormComponent> // from global config (or native form, if not provided)
      <RhfUtilsZodForm.Children />       // form instance component prop
    </RhfUtilsClientConfig.FormComponent>
  <RhfUtilsProviders>
</ReactHookForm.FormProvider>

De-couple Providers from Form

Sometimes you need to control the nesting of the Form element. For example, to appease HTML rules or expand the scope of the Provider.

<aside>
  <RhfUtilsZodFormProviders formId="my-form">
    <main>
      <RhfUtilsZodForm />
    </main>

    <footer>
      <SubmitButton form="my-form">

      <MyFormErrorsViaProviders>
    </footer>
  </RhfUtilsZodFormProviders>
</aside>

To access forms' state outside its respective context, including in aggregate across more than one form, see Form State Relay section.

FormSubmitError

This is an Error-based class that you can use to throw a schema-typed error in your submit handler.

(Internally, it uses FormSubmitFieldErrors type's structure, which is a flat, simplified version of RHF's FieldErrors. It differs from FlatFieldError only in that it is narrower. It allows field names from your schema and root.${string} keys. And only type (optional) and message props for error.)

Example

<RhfUtilsZodFormWithProviders
  onBeforeSubmit={async ({ input, output, api }, { FormSubmitError }) => {
    if (!isValid(data))
      throw new FormSubmitError({
        'street.address': { message: 'Street address invalid.' },
      });
  }}
/>

Any non-FormSubmitError error thrown from your submit handler (e.g., fetch/axios error) can be transformed by RhfUtilsClientConfig's onSubmitErrorUnknown callback. This takes an unknown error and can return a FormSubmitFieldErrors object, which is merged into RHF's form state errors.

Most common use case will be transforming backend errors to frontend shape expected by RHF.

Last Submit Error

Sometimes, perhaps outside any specific FormContext, you need direct access to the actual error object that was thrown, which caused the last submit to fail. The useLastSubmitError hook allows you to do just this. (You can probably handle most form-specific cases via onSubmitErrorUnknown, which receives error as well.)

RhfUtilsFormOptions

These built-in options can be set globally and/or per form.

type RhfUtilsFormOptions = {
  /** Stop propagation of submit event. */
  stopSubmitPropagation?: boolean;

  /** Request submit on change. */
  submitOnChange?: { debounce?: number };

  /**
   * Reset form values and state (e.g., isDirty, etc.) after submit -- on success and/or error.
   * - `defaults`: reset current values to defaults (e.g., clear form)
   * - `current`: reset defaults to current values (keep current values)
   */
  resetOnSubmitted?: {
    success?: { values: 'defaults' | 'current' };
    error?: { values: 'defaults' };
  };

  /** Control dev tool options. (Lazy-loaded when truthy value supplied.) */
  devTool?: boolean | Pick<DevtoolUIProps, 'placement' | 'styles'>;
};

If you need access to options deeper in your component structure, use useRhfUtilsContext to receive RhfUtilsContext object, which includes formId, formRef, options, and lastSubmitStateRef.

lastSubmitStateRef (a ref with possible value of null | 'submitting' | 'success' | 'error') conveys current submit state via ref -- i.e., without needing to wait for next render cycle. If your form navigates away via onSubmitSuccess, RHF's useFormContext still shows form as isDirty and isSubmitting, which makes it difficult to distinguish from a user-initiated navigation before form is submitted successfully.

Use useRhfUtilsContextRequestSubmit hook to get requestSubmit function for current form ref in context. This is useful when you need to trigger form submission programatically.

Extend RhfUtilsFormOptions

Extend RhfUtilsFormOptions with custom options, which get passed to RhfUtilsClientConfig's ChildrenWrapper and RhfUtilsZodForm's Children components via options prop. These can take any shape, and allow you to override your own functionality at form-level.

import '@paragrav/rhf-utils';

declare module '@paragrav/rhf-utils' {
  export interface Register {
    RhfUtilsFormOptions: {
      /** Custom props for your custom options/hooks/behaviors per form instance. */
      enableMyPrompter?: boolean;
      enableMyOptionalFormHook?: boolean;
      configMyOptionalFormHook?: MyCustomFormHookConfig;
    };
  }
}

Form State Relay

Sometimes you need to access one or more forms' state outside its respective context.

FormRelayContextProvider is included by default, but form's state is only relayed with opted-in. Configuration is done per form instance via relay prop.

type FormRelayOptions = {
  /**
   * Determines what part of RHF's form state is relayed.
   * Also acts to activate RHF proxy values.
   */
  select: (formState: FormState<SafeFieldValues>) => FormRelayStateSelected;

  /**
   * Arbitrary group name(s) for this form.
   * Think of it like class or category names.
   * (e.g., "parent" or "children")
   */
  groups?: string[];
};

There are two main ways to get form state that has been relayed.

Get form relay by id

You can retrieve a singular form's relay state via useFormRelayId(formId). This will return a FormRelay object with that form's selected state (FormRelayStateSelected), options (FormRelayOptions), and utils (RhfUtilsContext).

Get form relay by group

When retrieving by group, state is merged into a FormRelayGroup object (and no options or utils are provided, as these are form-specific).

Use useFormRelayGroup(callback) and providing a callback that is is supplied with options (FormRelayOptions) and utils (RhfUtilsContext) of each form registered to relay, in order for you to determine whether it should be included in this arbitrary "group".

You can also use FormRelayGrouperContextProvider if you want to make this group accessible via naive hook.

<FormRelayGrouperContextProvider
  predicate={({ options, utils }) =>
    !!options?.groups?.includes("parent") // find specific group
  }
>
  {(state: FormRelayGroup) => ()}
</FormRelayGrouperContextProvider>

Alternatively, instead render callback, you can use useFormRelayGrouper.

If you want to build some components to be independent of any one specific context -- i.e., RHF's context, FormRelayGrouperContext, or FormRelayContext -- you can use useFirstFormStateGroup to get FormRelayGroup from nearest available context. (If RHF's context is found, its singular state is transformed to FormRelayGroup.)

Field Errors

FlatFieldErrors

FlatFieldErrors type is a flattened, simplified version of RHF's FieldErrors. Keys represent flattened, dot-notation field paths.

Use useFlatFieldErrorsContext() hook, which returns an object with errors grouped by all, fields, roots, orphans records, and boolean values for hasErrors and hasOrphans.

Output

For the purposes of debugging and/or logging, you can configure when form state errors are outputted (i.e., console and/or thrown) via RhfUtilsClientConfig.fieldErrors.output (example at the top).

Orphan Errors

The concept of an "orphan" error is any errant schema property that could prohibit users from submitting a valid form because its input is missing or non-existent.

Programatically, an "orphan" is any form state error that meets ALL of the following criteria:

  • non-field -- i.e., no RHF-supplied ref on FieldError object
  • non-root -- i.e., not root or root.${string} path
  • no corresponding "marker" in DOM (i.e., RhfUtilsNonFieldErrorMarker)

This is because:

  • field errors (with refs) are assumed to be displayed next to their respective input
  • root errors (non-field, without refs) are assumed to always be listed for display
  • all other errors must be marked as displayed to distinguish from being an orphan

See RhfUtilsNonFieldErrorMarker section below for more information about when and why marker is needed.

Using orphan errors

Detected orphans can be accessed via any of the following:

  • RhfUtilsClientConfig.fieldErrors.output -- e.g., config per environment:
    • development: throw to facilitate discovery and debugging
    • production: console.error to facilitate reporting
  • useFlatFieldErrorsContext() hook
    • returns an object with list of errors grouped by all, fields (with ref), roots, orphans records, and includes computed booleans hasErrors and hasOrphans
  • boolean value from useFlatFieldErrorsContextHasOnlyOrphans

RhfUtilsNonFieldErrorMarker

If you don't need orphan detection, you can skip this section. By default, orphan detection still occurs but doesn't otherwise do anything.

To get accurate orphan detection, you must use RhfUtilsNonFieldErrorMarker when displaying any non-root non-field errors. (Example further below.)

There is little harm in including it consistently for all individual errors displayed. (DOM traversal to find marker only occurs if error is non-field AND non-root, which is not typical.)

When is the marker required? (example of non-root non-field error)

A typical example is a field array with a required minimum number of items -- e.g., items: z.array(...).min(1).

When there are zero items, the error is non-field and non-root. You would probably display this error (manually) near the field array, using something like RHF's ErrorMessage component.

In order to NOT detect this as an orphan, it must be explicitly "marked" as displayed using RhfUtilsNonFieldErrorMarker.

You should incorporate RhfUtilsNonFieldErrorMarker into your own component library's error message display component.

<RhfUtilsNonFieldErrorMarker path="items" />

If you include the marker in bulk error lists, it will defeat the purpose of the marker. Only use marker for individual errors. Therefore, if you are ONLY listing a summary of all errors, without individual displays, you will need to use the marker manually.

Other Features

TRPC.io Utils (0.41kB min+gzip)

trpcClientErrorToFormSubmitFieldErrorsSchemaTransformer

zod-based schema for transforming TRPCClientErrorLike<AnyRouter> server errors to FormSubmitFieldErrors.

makeOnSubmitTrpcClientErrorHandler

HOF to make a onSubmitErrorUnknown handler for TRPC.io server errors (with fallback callback for non-TRPC errors).

  onSubmitErrorUnknown: makeOnSubmitTrpcClientErrorHandler((error) => {
    // non-trpc error
  }),

Other

SafeFieldValues

This library uses SafeFieldValues type which uses unknown instead of any.

Peer dependencies