Rafael Camargo

Validando formulários React facilmente sem bibliotecas

Apesar dos avanços relacionados ao gerenciamento de formulários na versão 19 do React, a triste verdade é que validar um formulário continua sendo uma tarefa chata e repetitiva. A intenção deste post é oferecer uma abordagem versátil o suficiente para que possa ser aplicada em qualquer formulário e reduza ao máximo o esforço e o tédio que é o trabalho de validação.

A abordagem se baseia em três etapas que são executadas toda vez que os dados do formulário são atualizados:

Diagrama exibindo o fluxo de validação de um formulário React
Diagrama do fluxo de validação de um formulário

A divisão de responsabilidades estabelecida por esta estratégia, delimitadas na ilustração acima pelos blocos A e B, permite que B seja extraído para seu próprio módulo, podendo ser reaproveitado ao longo de toda a aplicação.

Nos demais formulários, a preocupação se restringirá apenas à definição do Form Data e das Validations, dados que dependerão sempre do contexto de cada formulário.

Estrutura

Form Data: Variável de estado que representa os dados do formulário em forma de uma objeto contendo pares de chave/valor, no qual chave é o name do campo do formulário:

const formData = {
  [NAME]: '',
  [EMAIL]: ''
};

Validations: Objeto com estrutura similar ao formData, mas que, ao invés de pares chave/valor, contém uma lista de validadores e suas respectivas mensagens de erro. A estratégia aqui é que o valor inserido no campo do formulário passe por cada uma das validações e que, caso uma das validações falhe, a respectiva mensagem de erro seja exibida junto ao campo.

const validations = {
  [NAME]: [{
    isValid: val => !!val,
    errorMessage: 'Required'
  }, {
    isValid: val => val?.length >= 2,
    errorMessage: 'Enter at least 2 characters'
  }],
  [EMAIL]: [{
    isValid: val => !!val,
    errorMessage: 'Required'
  }, {
    isValid: val => isEmailValid(val),
    errorMessage: 'Enter a valid email address'
  }]
};

function isEmailValid(email){
  const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return regex.test(email);
};

Validator: O validador é o mecanismo responsável por reduzir o formData através de cada uma das validações e produzir o objeto formErrors:

export const validateForm = (formData, validations) => {
  return Object
    .entries(validations)
    .reduce((formErrors, [fieldName, fieldValidations]) => {
      const err = validateField(formData[fieldName], fieldValidations);
      return err ? { ...formErrors, [fieldName]: err } : formErrors;
    }, {});
}

function validateField(fieldValue, fieldValidations){
  return fieldValidations
    .reduce((fieldError, { isValid, errorMessage }) => {
      if(fieldError) return fieldError;
      return !isValid(fieldValue) ? errorMessage : fieldError
    }, '');
}

Form Errors: Objeto contendo pares de chave/valor que representa os erros do formulário, no qual chave é o name do campo do formulário e valor é a mensagem que explica o erro corrente do campo. Os campos que estiverem válidos não serão incluídos no formErrors, ou seja, quando o formulário estiver 100% válido, formErrors será um objeto vazio ({}):

const formErrors = {
  [NAME]: 'Required',
  [EMAIL]: 'Required'
};

O caso acima simula a situação na qual nenhum dos campos do formulário foi preenchido.

Exemplo

Abaixo você confere a aplicação da abordagem a um componente que chamei de NewsletterForm, um formulário que contém dois campos obrigatórios: Name e Email.

import { useState } from 'react';
import { validateForm } from './form-validator';
import { isEmailValid } from './email-validator';

const NAME_FIELD_NAME = 'name';
const EMAIL_FIELD_NAME = 'email';

const NewsletterForm = () => {
  const [formData, setFormData] = useState({});
  const [hasSubmitted, setSubmission] = useState();
  const validations = buildValidations();
  const formErrors = validateForm(formData, validations);
  const handleChange = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
  };
  const buildErrorMessageEl = fieldName => {
    const message = formErrors[fieldName];
    return hasSubmitted && message && <span>{message}</span>;
  }
  const handleSubmit = evt => {
    evt.preventDefault();
    setSubmission(true);
    if (Object.keys(formErrors).length === 0) {
      alert('Form submitted!');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          aria-label="Name"
          placeholder="Name"
          name={NAME_FIELD_NAME}
          value={formData[NAME_FIELD_NAME] || ''}
          onChange={handleChange}
        />
        {buildErrorMessageEl(NAME_FIELD_NAME)}
      </div>
      <div>
        <input
          aria-label="Email"
          placeholder="Email"
          name={EMAIL_FIELD_NAME}
          value={formData[EMAIL_FIELD_NAME] || ''}
          onChange={handleChange}
        />
        {buildErrorMessageEl(EMAIL_FIELD_NAME)}
      </div>
      <button type="submit">Subscribe</button>
    </form>
  );
};

function buildValidations(){
  return {
    [NAME_FIELD_NAME]: [{
      isValid: val => !!val,
      errorMessage: 'Required'
    }, {
      isValid: val => val?.length >= 2,
      errorMessage: 'Enter at least 2 characters'
    }],
    [EMAIL_FIELD_NAME]: [{
      isValid: val => !!val,
      errorMessage: 'Required'
    }, {
      isValid: val => isEmailValid(val),
      errorMessage: 'Enter a valid email address'
    }]
  };
}

export default NewsletterForm;

Abordagem de validação aplicada a um formulário de Newsletter

Mão na massa: Neste Gist você encontra, em um único arquivo HTML, todo o código necessário para rodar localmente o exemplo apresentado no vídeo acima.

Saiba mais: Se você curtiu este post, talvez se interesse em saber como testar e implementar rolagem suave em links de navegação para a própria página com React.

Todo mês, uma boa dica de programação pro seu dia a dia.

Você pode ser notificado também por RSS