JSPM

  • Created
  • Published
  • Downloads 5
  • Score
    100M100P100Q75787F
  • License MIT

Integration utilities for react-hook-form.

Package Exports

  • @paragrav/rhf-utils

Readme

README

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 config.tsx with desired configuration settings. This is global configuration across all forms.

export const rhfUtilsClientConfig: RhfUtilsClientConfig = {
  isDevelopment: import.meta.env.DEV,

  // defaults -- overridable per form instance
  defaults: {
    // globally relevant subset of RHF's `UseFormProps` options
    rhf: {
      mode: 'onBlur',
    },

    // RhfUtilsFormOptions (see further below)
    utils: {
      // enable dev tool
      devTool: true,
    },
  },

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

  // transform backend error
  onUnknownSubmitError: (
    error, // unknown
  ) => {
    // if other error, re-throw to handle in ErrorBoundary
    if (!(error instanceof MyServerError)) throw error;

    // return to be merged into RHF context errors
    return transformServerErrorToFormSubmitErrors(error);
  },

  Children: {
    top: (
      // UseRhfUtilsFormChildrenProps
      {
        formId, //          unique id string
        formRef, //         ref
        context, //         rhf UseFormReturn
        options, //         RhfUtilsFormOptions
        Controller, //      strongly-typed controller
        FormSubmitError, // strongly-typed error class
      },
    ) => {
      // navigation blocker (using custom option)
      useFormRouterBlocker(!!options?.prompter);

      return null;
    },

    bottom: () => (
      <>
        {/* global error list */}
        <RootErrorsListFromFlatFieldErrorsContext />
      </>
    ),
  },

  errors: {
    output: {
      // callback to determine when to output form context errors to console ("debug" or "error")
      console: ({ isDevelopment, hasOrphans }) =>
        (hasOrphans && { type: 'error' }) || // report all orphan errors
        (isDevelopment && { type: 'debug' }), // facilitate debugging on dev

      // callback to determine when to throw an error based on context
      throw: ({ isDevelopment, hasOrphans }) => isDevelopment && hasOrphans,
    },
  },
};

Provider

And add the context provider to your stack:

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

Currently, only zod is supported.

Usage

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

      <button type="submit">Login</button>
    </>
  )}
/>

Children is a component which is rendered as a child of the form. It receives UseRhfUtilsFormChildrenProps as props, including type-safe Controller.

If you prefer to define your Children component as standalone:

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

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

FormSubmitError

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

It uses FormSubmitErrors shape, which is a flat (dot notation) equivalent of RHF's FieldErrors.

Example:

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

Any non-FormSubmitErrors thrown from submit handler can be transformed by RhfUtilsClientConfig's onUnknownSubmitError 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 for TRPC backends.)

RhfUtilsFormOptions

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

type RhfUtilsFormOptions = {
  /**
   * Control visibility of certain items via your own classes and styles.
   * (Defaults merged with individual class names.)
   */
  classNames?: string[];

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

  /** Reset form values if submit throw errors. (Use case: optimistic UI toggle form that submits on each change.) */
  resetValuesOnSubmitError?: boolean;

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

  /** Reset form after successful submit. */
  resetFormAfterSubmitSuccessful?: Parameters<
    typeof useResetFormAfterSubmitSuccessful
  >[0];

  /** Control dev tool options. (Lazy-loaded when `isDevelopment` is `true` and truthy value supplied. Zero bundle size in prod.) */
  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 to get requestSubmit fn 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.

import '@paragrav/rhf-utils';

declare module '@paragrav/rhf-utils' {
  export interface Register {
    RhfUtilsFormOptions: {
      /** Enable user prompt to confirm navigating away from dirty form. */
      prompter?: 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.

Orphans

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 which 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 optionally exposed in a few places.

  • In errors outputted via console.
  • And useFlatFieldErrorsContext() hook, which returns an object with divided into all, root, and orphans errors.

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

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.