JSPM

  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q72095F
  • License MIT

Integration utilities for react-hook-form.

Package Exports

  • @paragrav/rhf-utils

Readme

README

About

Integration and utility library for react-hook-form.

If you have multiple forms and would like a more declarative API to manage their behavior (via built-in and custom options), including transformation of backend errors, and debugging logging/errors.

Features

  • built with and for TypeScript
  • global and form-specific configuration/options
    • with support for extendable options
  • schema-typed Controller component
  • schema-typed FormSubmitError class
  • safer FieldValues type (i.e., SafeFieldValues)
  • flatter FieldErrors structure (i.e., FlatFieldErrors)
  • zod support
    • includes distinction between z.input and z.output types for transformations support
  • 3.3kB gzip core functionality (excluding peer dependencies)

Install

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

Config

To configure, create a file like config.tsx with desired configuration settings. This is global configuration across all forms, some of which can be overridden at the form level.

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

    // RhfUtilsFormOptions (see further below)
    utils: {
      stopSubmitPropagation: true,
      devTool: import.meta.env.DEV,
    },

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

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

  // optional wrapper to inject your own hooks and components
  // around all RhfUtilsZodForm instances
  FormChildrenWrapper: (
    // UseRhfUtilsFormChildrenProps
    {
      formId, //          unique id string
      formRef, //         ref
      context, //         rhf UseFormReturn
      options, //         RhfUtilsFormOptions (with any custom props)
      Controller, //      strictly-typed rhf controller
      FormSubmitError, // strictly-typed error class
      children, //        RhfUtilsZodForm's Children instance
    },
  ) => {
    // your own hooks/behaviors controlled via custom option props
    // (see "Extend RhfUtilsFormOptions" section for more info)
    useMyFormNavigationPrompt({
      enabled: !!options?.enableMyFormNavigationPrompt,
    });

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

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

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

    // if other error
    return {
      root: { type: 'server', message: 'There was a problem.' },
    } satisfies FormSubmitErrors;
  },

  // rhfContext.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,
    },
  },
};

Provider

And add the context provider to your global provider stack:

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

Currently, only zod is supported.

Form

<RhfUtilsZodForm
  schema={loginFormSchema}
  defaultValues={{
    email: '',
  }}
  // submit handler
  onSubmit={async (
    data, //    schema output
    context, // UseRhfUtilsFormOnSubmitContext (id, ref, rhf context, utils options, strictly-typed FormSubmitError class)
    event, //   SubmitEvent
  ) => {
    await loginService(data);
    props.onSuccess();
  }}
  // handle error declaratively (i.e., no throw/catch)
  onSubmitError={({
    error, //   unknown
    context, // UseRhfUtilsFormOnSubmitErrorContext (id, ref, rhf context, utils options)
    event, //   SubmitEvent
  }) => {
    props.onError(error);
  }}
  // fields
  Children={({
    // UseRhfUtilsFormChildrenProps
    formId, //          unique id string
    formRef, //         ref
    context, //         rhf UseFormReturn
    options, //         RhfUtilsFormOptions
    Controller, //      strictly-typed rhf controller
    FormSubmitError, // strictly-typed error class
  }) => (
    <>
      <Controller
        name="email" // strictly-typed field name
        render={({ field, formState: { isSubmitting } }) => (
          <label>
            Email
            <input {...field} disabled={isSubmitting} />
            <FormErrorMessageByPath path={field.name} />
          </label>
        )}
      />

      <button type="submit">Login</button>
    </>
  )}
  // form-specific options/overrides (merged with defaults)
  rhf={{
    mode: 'onBlur',
  }}
  utils={{
    submitOnChange: true,
    resetOnSubmitted: {
      onSuccess: { values: 'current' },
      onError: { values: 'defaults' },
    },
    // custom option props
    // (see "Extend RhfUtilsFormOptions" section)
    enableMyFormNavigationPrompt: true,
  }}
  form={{
    // class names are merged together with global defaults
    className: 'my-special-form-class',
  }}
/>

Form Children

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

If you prefer to define your Children component as standalone:

const Children: RhfUtilsUseFormChildrenZodFC<typeof schema> = ({ ... }) => { };

function Children({...}: RhfUtilsUseFormChildrenZodProps<typeof schema>) { }

Component Hierarchy

<ReactHookForm.FormProvider>
  <RhfUtilsProviders>
    <RhfUtilsClientConfig.FormComponent>
      <RhfUtilsClientConfig.FormChildrenWrapper>
        <RhfUtilsZodForm.Children />
      </RhfUtilsClientConfig.FormChildrenWrapper>
    </RhfUtilsClientConfig.FormComponent>
  <RhfUtilsProviders>
</ReactHookForm.FormProvider>

FormSubmitError

This is an Error-based class you can use to throw a structured error in your submit handler. It is strictly-typed, so it only allows field names from your schema and root/root.{string} keys.

It uses FormSubmitErrors type's structure, which is a flat, simplified version of RHF's FieldErrors.

Example:

<RhfUtilsZodForm
  onSubmit={async (data, { FormSubmitError }) => {
    if (isProblem(data))
      throw new FormSubmitError({
        root: { message: 'There was a problem with the form.' },
        'street.address': { message: 'Street address invalid.' },
      });

    await post('/backend');
  }}
/>

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

Most common use case will be transforming backend errors to frontend shape. (You can use getOnSubmitTrpcClientErrorHandler HOF provided by this library for TRPC backends.)

RhfUtilsFormOptions

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

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

  /** Request submit via listener on form change. */
  submitOnChange?: boolean;

  /**
   * 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?: true | Pick<DevtoolUIProps, 'placement' | 'styles'>;
};

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

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 Children component in 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: {
      /** Enable user prompt to confirm navigating away from dirty form. */
      enableMyFormNavigationPrompt?: boolean;
    };
  }
}

Errors

You can configure via RhfUtilsClientConfig (example at the top) when form context errors are outputted -- i.e., via console and/or thrown error.

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

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

Orphan Errors

The concept of orphan errors is any error that is not being shown to user. An example would be a stray field in a schema that is prohibiting users from successfully submitting a form.

The criteria for an orphan is any form context error that meets all of the following criteria:

  • not a root error -- e.g., root or root.${string}
  • has no ref -- RHF includes ref to the associated input on each error object (when applicable)
  • has no marker in DOM (e.g., FormNonFieldErrorMarker)

To get accurate orphan analysis, you must either use FormNonFieldErrorMarker in any error you are displaying to user that is not "root" and doesn't have a ref. (There is no harm in using this consistently across all errors, even those expected to have a ref.)

Alternatively, you can use FormErrorMessageByPath to display error message to user:

<FormErrorMessageByPath path="street.address" />

Example use case: Field array with minimum items. When there are no items, the error is not associated with a field and is displayed separately.

Orphans are exposed in a few places.

  • In errors outputted via console. (Configurable via RhfUtilsClientConfig['errors']['output'].)
  • And useFlatFieldErrorsContext() hook, which returns an object with errors grouped by all, fields, roots, orphans records, and hasErrors and hasOrphans boolean.
  • Boolean value from useFlatFieldErrorsContextHasOnlyOrphans.

Form Groups

Sometimes you need to group multiple forms together.

Use case: disable parent form when any child is "busy"; disable all children when parent is "busy".

Use FormGroupContextProvider to wrap your children and parent forms.

<FormGroupContextProvider>
  <ChildForm1 />
  <ChildForm2 />
  <ParentForm />
</FormGroupContextProvider>

In your parent form, use the hook useFormGroupParentTracker, which returns boolean value indicating whether any child forms is busy.

In children forms, there are two options. You can choose to consider form "busy" when:

  • isSubmitting is true by using useFormGroupChildIsSubmittingTracker hook
  • it is mounted by using useFormGroupChildIsMountedTracker hook

Both hooks return boolean value indicating whether parent form is busy.

Other

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

Peer dependencies: