react-hook-form: Helper methods for errors object

Is your feature request related to a problem? Please describe. Many component libraries have a property on their fields which decide whether is is invalid or not. For example, in the case of material ui this is the “error” property. It’s a boolean, so you always need a != null there, as errors.fieldName is an object, not a boolean.

Also when writing the error message somewhere you’ll always have to check if the object at errors.fieldName exists before you access the message at errors.fieldName.message, or else you’ll get an exception if there’s no error.

This looks the following in material ui:

error={errors.fieldName != null} helperText={errors.fieldName && errors.fieldName.message}

Describe the solution you’d like I think this could be solved by adding two simple helpers. error.message(‘fieldName’) -> Returns either the error message or undefined, if there’s no error error.has(‘fieldName’) -> Returns true if there’s an error, or else false

Then you could go like the following:

error={errors..has('fieldName')} helperText={errors.message('fieldName')}

I think that’s more clean and simple.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 26 (15 by maintainers)

Most upvoted comments

I am using Material UI + TypeScript + react-hook-form and I do this:

error={!!errors.fieldName} helperText={<FieldError error={errors.fieldName} />}

The !! makes it a boolean, which is required in MUI’s types. Then the component for helperText gives me one place to handle the rendering for all form fields. In there I can check to see if there is a message to show. If there is no error message to show, then FieldError just returns an empty fragment: <></>.

@bluebill1049 I’d like to +1 this.

I would like to add such a helper but at the same time, it increases the bundle size of this lib […]

I don’t think 2 helper functions with 1 or 2 lines each add significant overhead in terms of bundle size.

I just discovered this library and so far I think it is awesome, however, I do not think it is reasonable to ask users to include something like lodash for very simple functionality that is basically the core of the lib.

I’ve implemented helpers like this:

const hasError = inputName => Boolean(errors && errors[inputName]);
const getError = inputName => (hasError(inputName) ? errors[inputName].message : '');

Problem is that such functions are not pure, since they close over the errors object which is returned from useForm. So I end up with one of the following scenarios:

1. Declare such helper functions inside every component I have

Problem: it is very repetitive and boring having to copy and paste it for every form I have. Some applications might have dozens of different forms.

2. Make the functions pure

And use currying to ease the use:

const hasError = errors => inputName => Boolean(errors && errors[inputName]);
const getError = errors => inputName => (hasError(inputName) ? errors[inputName].message : '');

Then in my component I would have:

const { register, handleSubmit, errors } = useForm();
const formHasError = hasError(errors);
const formGetError = getError(errors);

Problem: This would also be very repetitive if spread throughout many forms in a given application.

3. Using lodash

I would have to do something like:

<TextField
    error={_.has(errors, 'email')}
    helperText={_.get(errors, 'email.message', '')}

Problem: if I don’t use lodash anywhere else in the project, it is yet another dependency that I’ll have to add. I’m not even talking about the bundle size impact of adding lodash, because we have tree-shaking. I’m simply advocating against adding another dependency itself.


⚠️ Main problem ⚠️

With any of the above approaches, I would be creating a coupling between my app and this lib’s internals. I’m assuming that errors is a plain object with a message property inside an email one.

This might hold true for now, but maybe in the future you can come up with another internal structure for this library, which would break my application. It would be very painful to change all my forms to comply to the new structure.

Ideally, you should probably not export the raw errors object at all. What I think would be useful for most users would be something like the following interfaces:

type ValidationPredicate = (value: string | boolean | number) => boolean;

type ValidationType = |
  | 'required'
  | 'maxLength'
  | 'minLength'
  | 'max'
  | 'min'
  | 'pattern'
  | ValidationPredicate
  | Record<string, ValidationPredicate>;

interface ValidationError {
  type: ValidationType;
  types: Record<string, string | boolean>;
  message: string;
  ref: React.RefObject;
}

interface Validation {
  hasErrors(): boolean;
  hasError(name: string): boolean;
  getErrorMessage(name: string): string;
}

If you really need to expose the errors object (maybe to not break retro-compatibility), then there could have something like:

type ErrorsObject = Record<string, ValidationError> & Validation;
const errors: ErrorsObject = { /* ... */ };

Usage would be like:

const { validation, register, handeSubmit } = useForm();
// ...
<TextField
    error={validation.hasError('email')}
    helperText={validation.getErrorMessage('email')}

Since the code that returns the Validation object closes over the raw errors object, you could easily export the detached versions of those methods. Then usage could be more like:

const { hasError, getErrorMessage, register, handeSubmit } = useForm();
// ...
<TextField
    error={hasError('email')}
    helperText={getErrorMessage('email')}

Here’s my FieldError component.

The main thing it does for me right now is convert validation errors, which are camelCase, into user friendly errors. Long story but this is due to how i18n works in my app. The general idea is that it does whatever processing and styling is needed for the error before displaying it.

import React from 'react';
import { FieldError as ReactHookFormFieldError } from 'react-hook-form/dist/types';

// This is my own form context (separate from react-hook-form's context).
import { useFormContext } from '../Form';

type Props = {
  error?: ReactHookFormFieldError;
};

const FieldError = ({ error }: Props) => {
  const formContext = useFormContext();

  if (!error || !error.message) {
    return <></>;
  }

  /* Get the user friendly error from the errorMap */
  const errorMessage = formContext.errorMap[error.message] || error.message;

  return <>{errorMessage}</>;
};

export { FieldError };

I am so sorry, I didn’t meant to be petty about names, but in this case the name helps us to understand what is going on, regarding patterns that emerged from React hooks and the ones that were kind of discontinued.

The object returned from useForm() already has a setError and a clearError message. It would make sense getError (or getErrorMessage for clarity) and hasError to be present here as well.

I think having another component as a dependency just to handle these cases is too much. Furthermore, it is unintuitive. The main issue I see here is the asymmetry of the APIs.

That is very different from react-hook-form-input. In that case, it actually makes sense because you are fundamentally changing something at the core of the library: the choice of uncontrolled components, to allow users to handle controlled components more easily, without having to deal with useEffect and other stuff.

hasError and setError don’t change anything at all, in fact, they are just a façade to what you already have. They provide users less coupling and could make your life easier as a maintainer, since you would not be exposing the internals of the lib.

Again, I am sorry for making a big deal about this. As I said, I have just discovered this library and I its DX is so much better than the others available. That’s the one little issue bugging me 😅.

Anyway, I don’t mean to impose anything. I would be happy with the custom hook as well. Probably I will never make it a package though.

I don’t think adding a HOC would be the right approach here. HOCs are exactly one of the things that hooks made deprecated.

In case such helper methods cannot be added to this library, I’ll probably define a custom hook like this and use it:

import { useCallback } from 'react';

export default function useFormEnhanced({
  mode,
  reValidateMode,
  defaultValues,
  validationSchema,
  validationSchemaOption,
  validateCriteriaMode,
  submitFocusError,
  nativeValidation
}) {
  const {
    register,
    unregister,
    errors,
    watch,
    handleSubmit,
    reset,
    setError,
    clearError,
    setValue,
    getValues,
    triggerValidation,
    formState
  } = useForm({
    mode,
    reValidateMode,
    defaultValues,
    validationSchema,
    validationSchemaOption,
    validateCriteriaMode,
    submitFocusError,
    nativeValidation
  });

  const hasError = useCallback(inputName => Boolean(errors && errors[inputName]), [errors]);
  const getError = useCallback(inputName => hasError(inputName) ? errors[inputName].message : "", [errors]);

  return {
    register,
    unregister,
    watch,
    handleSubmit,
    reset,
    setValue,
    getValues,
    triggerValidation,
    formState,
    setError,
    clearError,
    hasError,
    getError
  };
}

hey, @hbarcelos sorry for the late reply (too many things in my hands at the moment 🤘) thanks for your detailed feedback. I didn’t force everyone to install Lodash (definitely not my intention only if your project already includes it) 😃 in fact, I wrote a small section for Error Message:

If your project is using lodash, then you can leverage what lodash get function. Eg:

https://react-hook-form.com/advanced-usage#ErrorMessage

recently there is a new ES feature release which probably would help us better to deal with error message: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining i will include that in the website soon.

However, you have some valid and good points there, and i don’t mind to have a generic component to handle those errors. similar for what i did for controlled inputs:

https://github.com/react-hook-form/react-hook-form-input

if you would like to create such a component (embed some of the logic inside without growing the core lib) and i am more than happy to include on the website and let users know to use it if it makes their life easier.

again thanks very much for your constructive feedback 👍