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
FieldValuestype (i.e.,SafeFieldValues) - flatter
FieldErrorsstructure (i.e.,FlatFieldErrors) zodsupport- includes distinction between
z.inputandz.outputtypes for transformations support
- includes distinction between
- 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 # yarnConfig
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.,
rootorroot.${string} - has no
ref-- RHF includesrefto 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 byall,fields,roots,orphansrecords, andhasErrorsandhasOrphansboolean. - 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:
isSubmittingistrueby usinguseFormGroupChildIsSubmittingTrackerhook- it is mounted by using
useFormGroupChildIsMountedTrackerhook
Both hooks return boolean value indicating whether parent form is busy.
Other
This library uses SafeFieldValues type which uses unknown instead of any.