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 # yarnConfig
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.,
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 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 intoall,root, andorphanserrors.
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:
isSubmittingistrueby usinguseFormGroupChildIsSubmittingTrackerhook- it is mounted by using
useFormGroupChildIsMountedTrackerhook
Both hooks return boolean value indicating whether parent form is busy.