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 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.