Criando Um Formulário Customizado com React Hook Form
Olá a todos que estão lendo, meu nome é Rian Aquino, sou Desenvolvedor FullStack Trainee na FCamara, e nesse artigo vamos fazer juntos um formulário em React e TypeScript utilizando a biblioteca React Hook Form e fazendo toda a validação com o Yup!
Antes de iniciarmos, é bom deixar avisado que não trataremos nesse artigo sobre as partes de instalação e configuração do projeto e das bibliotecas. Iremos pular direto para a parte da construção dos componentes, com exceções de algumas configurações pontuais. Então segue uma listinha aqui abaixo com todas as dependências do projeto e um link para cada documentação com o passo a passo simples de como configurá-las no seu projeto. 😁
// O projeto foi criado utilizando Vite
"react-hook-form": "^7.43.9", // https://react-hook-form.com/get-started/
"yup": "^1.1.1" // https://www.npmjs.com/package/yup
"tailwindcss": "^3.3.2", // (Opcional) https://tailwindcss.com/docs/installation
Introdução ao React Hook Form
Tudo pronto para continuarmos? Vamos começar montando um exemplo básico com o React Hook Form…
Vamos pegar o seguinte formulário como base:
export function FormPage() {
return (
<form>
<label>
Nome:
<input name="nome" placeholder="digite seu nome" />
</label>
<label>
E-mail:
<input name="email" placeholder="digite seu email" />
</label>
<button type="submit">Enviar</button>
</form>
);
}
Um formulário comum! Ele tem dois campos: nome e e-mail, e um botão para enviar os dados. Agora vamos entender como o React Hook Form faz para poder interagir com um formulário do HTML…
Para fazer isso precisamos utilizar o Hook useForm()
da biblioteca, que nos retorna todos os métodos disponíveis para podermos construir, controlar e interagir com o nosso formulário e seus campos.
// Acessando por meio de destructuring podemos utilizar
// esses métodos diretamente
const { register, handleSubmit, /*...*/} = useForm();
Esses métodos mostrados no exemplo acima, são os métodos essenciais para que a biblioteca consiga funcionar. O register()
é o método responsável por “registrar” cada campo no formulário, dando para a biblioteca a possibilidade de manipulá-lo e acessar seu valor, enquanto o handleSubmit()
é o método da biblioteca de executar alguma função após o envio de um formulário.
Adicionando esses métodos ao nosso formulário, ele fica assim:
export function FormPage() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<label>
Nome:
<input placeholder="digite seu nome" {...register("nome")} />
</label>
<label>
E-mail:
<input placeholder="digite seu email" {...register("email")} />
</label>
<button type="submit">Enviar</button>
</form>
);
}
Perceba que foram removidos os atributos name dos campos, já que eles são adicionados como parâmetro do método register()
que retorna tanto o atributo de name de volta para o input, quanto alguns outros atributos a mais.
Pronto, agora se digitarmos nesses campos, apertarmos no botão e abrirmos o console…
Tcharam! Nosso handleSubmit funcionou e já retornou nossos campos, a biblioteca assumiu o controle do formulário, tudo nos conformes! 😉
Componentização
Agora vamos deixar as coisas mais difíceis, precisamos falar de componentização com essa biblioteca!
Pensa numa situação onde temos campos mais complexos, com estilização, subcomponentes, padrões em label e em como mostram suas mensagens de erro. Não fica agradável ficarmos replicando linhas e linhas de código pra cada campo que formos criar, não é mesmo? Fica ruim pra quem tá programando, pra quem tá lendo, enfim, só nos resta uma saída: componentizar!
Começando pelo próprio Form, criaremos um componente reutilizável de form utilizando do FormProvider da biblioteca, que nos provê por meio de um context, acesso mais fácil às suas propriedades dentro de outros componentes.
Dentro do componente de Form, precisamos receber então as propriedades onSubmit (poderíamos receber handleSubmit diretamente, mas vamos utilizar essa função internamente), methods (para repassarmos ao FormProvider), children, e o restante de atributos comuns para um form
.
Para isso vamos construir a interface FormProps:
interface FormProps<T extends FieldValues> extends PropsWithChildren, FormHTMLAttributes<HTMLFormElement> {
methods: UseFormReturn<T, any>;
onSubmit: SubmitHandler<FieldValues>;
}
Não é necessário, mas decidi torná-la um generic, para poder passar a tipagem dos nossos dados para os métodos que iremos receber.
Agora que temos a interface, basta construir o componente:
export function Form<T extends FieldValues>({
methods,
onSubmit,
children,
...rest
}: FormProps<T>) {
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} {...rest}>
{children}
</form>
</FormProvider>
);
}
Para dar um contexto antes de fazermos as mesmas coisas com os campos, precisamos entender a diferença entre um campo “controlled” e “uncontrolled”.
Um campo controlado ou controlled, é aquele campo que é controlado pelo componente pai, na grande maioria dos casos é o componente que vai receber um state como propriedade.
Enquanto um campo não controlado ou uncontrolled é aquele campo que é controlado pelo próprio DOM, como os nativos do HTML por exemplo. Nesse caso, o valor do campo é obtido por meio da referência DOM, o atributo ref
.
Agora que sabemos disso, a biblioteca do React Hook Forms trabalha com os dois tipos de campo de forma diferente. Como você pode ver, a biblioteca consegue trabalhar em cima de formulários nativos, e campos controlados pelo DOM. Na real, esse é o comportamento padrão da biblioteca, para ter suporte a campos controlados, é preciso usar um componente a parte o Controller.
Componente de Field
Terminamos o componente de Form, e sabemos a diferença de controlled e uncontrolled. Vamos fazer então o componente para um campo!
Um componente de campo nesse projeto vai exibir em tela a label do campo, o input, e a mensagem de erro. Observação para o input, é que será independente de componente nenhum, ou seja, construiremos um espaço para que no componente pai seja possível colocar qualquer elemento e suporte qualquer aninhamento, porém não iremos utilizar children, já já irão entender o porque.
interface FieldProps {
name: string;
label: string;
}
export function Field({ label, ...rest }: FieldProps) {
const {
formState: { errors },
} = useFormContext();
return (
<div>
<label className="text-lg text-gray-700 leading-relaxed mt-4">{label}</label>
{/* Input vai vir aqui dentro*/}
<span className="text-red-700">{errors[rest.name]?.message?.toString()}</span>
</div>
);
Aqui está nossa estrutura inicial, temos uma interface que possui os atributos name e label, e mais abaixo, de cara temos um hook novo: o useFomContext()
. Esse hook serve pra basicamente pegar as informações que foram passadas do FormProvider, que no caso, são os mesmos métodos retornados do useForm()
. Aqui nesse exemplo estamos pegando os errors de dentro do formState, e mais embaixo ainda, a partir dele, estamos resgatando a mensagem de erro específica desse campo.
Agora vem a parte complexa, adicionar o espaço do input. Para fazer isso vamos adicionar um atributo no nosso componente chamado render, que realmente vai servir para exibir o nosso input em tela, mas passando determinadas propriedades para que ele possa usar. Mas tenho um porém, não quero precisar fazer 2 componentes separados, visto que a forma como renderizamos os componentes controlados e não controlados são diferentes. Portanto, vamos partir o render em dois, dessa forma:
interface FieldProps {
name: string;
label: string;
render: {
controlled?: (props: {
field: Omit<ControllerRenderProps, "ref">;
fieldState: ControllerFieldState;
formState: UseFormStateReturn<FieldValues>;
}) => React.ReactElement;
uncontrolled?: (props: { field: UseFormRegisterReturn }) => React.ReactElement;
};
}
Temos então, dentro de render, 2 arrow functions que retornam um elemento React, mas que recebem propriedades diferentes.
Ok, mas da onde vêm essas propriedades? As propriedades de controlled vêm justamente do atributo render
do componente de Controller, já que esse componente precisa ser utilizado para conseguirmos utilizar campos controlados com a biblioteca. E o uncontrolled? As propriedades dele retornam diretamente do método de register()
, assim como da pra deduzir pelo nome do tipo de field.
Falta apenas criar a estrutura em si do espaço para o input, então vamos lá:
export function Field({ label, render, ...rest }: FieldProps) {
const {
register,
control,
formState: { errors },
} = useFormContext();
return (
<div>
<label className="text-lg text-gray-700 leading-relaxed mt-4">{label}</label>
{render.uncontrolled ? (
render.uncontrolled({ field: { ...register(rest.name) } })
) : (
<Controller
render={({ field: { ref, ...field }, ...props }) =>
render.controlled ? render.controlled({ field, ...props }) : <></>
}
control={control}
{...rest}
/>
)}
<span className="text-red-700">{errors[rest.name]?.message?.toString()}</span>
</div>
);
}
Explicando o que está acontecendo, agora que eu recebo render, eu vejo se a função uncontrolled foi definida, caso sim, eu executo essa mesma função, passando as propriedades do register
, que ficam disponíveis para os elementos que forem utilizados para o campo — veremos mais a frente. Caso não, então a opção escolhida foi de controlled, então eu retorno um componente de Controller, que possui a função própria de render (como dito anteriormente), confirmando para o TypeScript que a função foi definida e passando todas as propriedades necessárias.
Validação com Yup
Como esse artigo está ficando bem grande, vou passar rapidinho pelo Yup. Yup é uma biblioteca que te permite fazer schemas de validação, você consegue criar objetos, passando por cada campo e definindo seu tipo, suas regras de validação e as mensagens de erro.
Para usar o Yup, você importar usando o ‘*’ e já criar o schema para o seu formulário.
import * as Yup from "yup";
const schema = Yup.object({
nome: Yup.string().required(),
email: Yup.string().email().required(),
fruta: Yup.mixed().required(),
});
Porém, como estamos utilizando React Hook Form, precisamos realizar mais um passo, que é o de instalar o @hookform/resolvers
, mas basta usar o comando `npm i @hookform/resolvers`, que já está resolvido.
Seguindo, precisamos adicionar o resolver do Yup para o nosso formulário, passando o schema que a gente acabou de criar:
const methods = useForm({ resolver: yupResolver(schema) })
Pronto, agora o Yup já está funcionando, e gerenciando os erros para o React Hook Form. Nada muda para a maneira como estávamos retornando os erros, eles ainda pertencem ao React Hook Form e permanecessem sendo acessados no mesmo lugar, agora apenas são gerenciados pelo Yup.
OBS: Caso queira configurar as mensagens de erro em um schema, basta adicionar a mensagem como parâmetro em cada regra. E se quiser configurar mensagens padrões globalmente, dê uma conferida na função de setLocale()
. 😉
Bom, chegamos ao fim do artigo, muito obrigado por ter lido até aqui, e se você quiser ver o exemplo funcionando, com os novos componentes, a aplicação de um componente controlado com React Select, aqui 👈 está o link do GitHub com o projeto, um exemplinho besta, mas que da pra ver em prática!
Até mais pessoal, obrigado novamente e abraços! 🤗