DEV Community

Artem Lomonosov
Artem Lomonosov

Posted on

User input validation through React composition and custom hooks.

Let's say you have a relatively simple component. It renders HTML input and handles user submission.


type SimpleInputProps = {
  handleChange: Function
}

const handleChange = (value: string): void {
  pushToBackend(changeValue)
}

const SimpleInput = (props: SimpleInputProps): JSX.Element => {
  const { handleChange } = props

  return (
    <input type="text" onChange={handleChange} />
  )
}

You want to validate user input. Naturally, you don't want to hard-code the validation logic inside the component. You want to encapsulate it and use it through React composition. Eventually you need to get something like this:

const handleChange = ...

const SimpleInput = ...


<ValidationWrapper validations={validationList}>
  <SimpleInput handleChange={handleChange} />
</ValidationWrapper>

I must say, I don't want to use libraries for forms, because they are too heavy for my tasks right now.

So, we need to implement the ValidationWrapper component that encapsulates the validation logic.

As you can see, we want to pass validation handlers as ValidationWrapper properties.
The wrapper should take these handlers and decide to execute our handleChange function or throw error messages.

So how can we achieve this? Something like that:

type ValidationWrapperProps = {
  children: JSX.Element
  validations: Function[]
}

const ValidationWrapper = (props: ValidationWrapperProps): JSX.Element => {
  const { validations, children } = props
  // component must have a handler in props
  const originalHandler = children.props.handleChange

  const { errorMessages, patchedHandler } = useValidation(
    originalHandler, validations,
  )

  return (
    <>
      <children.type {...children.props} handleChange={patchedHandler} />
      {errorsMessages}
    </>
  )
}

What's going on here? We just put our input component in a validation wrapper and patch its handler with the useValidation hook. Yes, all magic lives on the hook. But it is already clear that this approach looks pretty compact. Let's take a look at the implementation of useValidation.

Actually, it can be anything. The main idea is to put the validation logic in one place.
I'll show the simplest example:

type ValidationHookProps = {
  callback: Function
  validations: Function[]
}

type ErrorMessages = string[]

const useValidation = (props: ValidationHookProps): ErrorMessages => {
  const { callback, validations } = props
  const [errorMessages, setErrorMessages] = React.useState<ErrorMessages>([])

  const patchedHandler = (changeValue: any): void => {
    const errors = validations.map((validate: Function) => validate(changeValue))

    if (!errors.length) return callback(changeValue)

    setErrorMessages(errors)
  }

  return { errorMessages, patchedHandler }
}

It's also pretty simple here. The hook creates a state to store error messages
that we grab from the validation function.

We also create a handleAction function. It calls validation handlers and receives messages from them. If we have errors, it won't call the original handler.

The useValidation hook returns a patched handler for validating user input and a list of error messages that you can use to display the error to your users.

Thus, we have achieved posibility to check user input through react components composition and custom hooks.

Top comments (0)