15 de junho de 2023 • 8 min de leitura
Integração de um Input de Moeda (ou qualquer input com máscara) com React Hook Form e Zod
Como integrar corretamente essas bibliotecas e suas limitações
Introdução
Este artigo tem como objetivo mostrar como você pode criar um input de moeda, ou qualquer outro input que você queira usando máscaras, comentando sobre as particularidades e detalhes que você deve prestar atenção para o funcionamento correto de todas as bibliotecas.
Para este exemplo, estou usando o React Number Format para o input com máscara, o React Hook Form para a construção e lógica do formulário, e o Zod para a validação.
Criando o Input de Moeda
Neste primeiro momento, apenas criamos o input usando a biblioteca React Number Format, assim:
import { NumericFormat } from 'react-number-format'
const DolarInput = () => {
return (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
/>
)
}
export default DolarInput
Até agora, nada de novo. É muito simples usar o react-number-format, e você pode consultar a documentação para obter mais possibilidades.
Integrando com o React Hook Form
Não é possível integrar diretamente com o React Hook Form, é necessário usar o componente Controller.
Normalmente, você usa o componente Controller passando o spread "field" para o input.
import { useForm, Controller } from "react-hook-form";
import { TextField, Checkbox } from "@material-ui/core";
function App() {
const { handleSubmit, control, reset } = useForm({
defaultValues: {
checkbox: false,
}
});
const onSubmit = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="checkbox"
control={control}
rules={{ required: true }}
render={({ field }) => <Checkbox {...field} />} // <= here
/>
<input type="submit" />
</form>
);
}
Mas não podemos acessar diretamente a ref do react-number-format. Se tentarmos usar o spread "field" diretamente, ocorrerá um erro porque o componente não está encaminhando corretamente as referências (refs):
Portanto, precisamos obter a ref dentro da propriedade "field" e usar a propriedade getInputRef do react-number-format e espalhar o restante dos parâmetros dentro do input, como no exemplo abaixo:
import { Controller, useForm } from 'react-hook-form'
import { NumericFormat } from 'react-number-format'
const DolarInput = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm()
const onSubmit = (data: any) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="currency"
control={control}
render={({ field: { ref, ...rest } }) => (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
getInputRef={ref}
{...rest}
/>
)}
/>
{!!errors && <span>{errors.currency?.message as string}</span>}
<button type="submit">Send</button>
</form>
)
}
export default DolarInput
Integração com o Zod
A abordagem normal é a seguinte e não tem observações:
import { zodResolver } from '@hookform/resolvers/zod'
import { Controller, SubmitHandler, useForm } from 'react-hook-form'
import { NumericFormat } from 'react-number-format'
import { z } from 'zod'
const formSchema = z.object({
currency: z.string()
})
type FormData = z.infer<typeof formSchema>
const DolarInput = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(formSchema)
})
const onSubmit: SubmitHandler<FormData> = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="currency"
control={control}
render={({ field: { ref, ...rest } }) => (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
getInputRef={ref}
{...rest}
/>
)}
/>
{!!errors && <span>{errors.currency?.message as string}</span>}
<button type="submit">Send</button>
</form>
)
}
export default DolarInput
Mas você pode ter alguns problemas se tentar usar outros recursos do Zod e, para isso, temos algumas diferenças.
Por exemplo, para usar defaultValues, precisamos passar uma string seguindo o mesmo padrão que enviamos o valor:
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormData>({
defaultValues: {
currency: '$ 10,000'
},
resolver: zodResolver(formSchema)
})
Você pode pensar em usar o Zod para transformar/coerce a string em um número usando uma função para remover os caracteres extras, mas quando você fizer isso, vai apenas achar que está funcionando:
import { zodResolver } from '@hookform/resolvers/zod'
import { Controller, SubmitHandler, useForm } from 'react-hook-form'
import { NumericFormat } from 'react-number-format'
import { transformCurrencyStrinToNumber } from 'utils/transformCurrencyStrinToNumber'
import { z } from 'zod'
const formSchema = z.object({
// hypothetic function that transform the string on number
currency: z.string().transform(transformCurrencyStrinToNumber)
})
type FormData = z.infer<typeof formSchema>
const DolarInput = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormData>({
defaultValues: {
currency: 10000
},
resolver: zodResolver(formSchema)
})
const onSubmit: SubmitHandler<FormData> = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="currency"
control={control}
render={({ field: { ref, ...rest } }) => (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
getInputRef={ref}
{...rest}
/>
)}
/>
{!!errors && <span>{errors.currency?.message as string}</span>}
<button type="submit">Send</button>
</form>
)
}
export default DolarInput
Render:
Mas quando você enviar o valor, verá que o campo de entrada não reconhece realmente o valor padrão e na verdade está vazio.
O react-number-format renderiza o valor formatado, mas não podemos alterar o valor do campo de entrada na primeira renderização, e isso é um problema.
E isso não ocorre quando trabalhamos apenas com strings, como no exemplo abaixo:
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormData>({
defaultValues: {
currency: '$ 10,000'
},
resolver: zodResolver(formSchema)
})
Enviar após a primeira renderização:
Portanto, sugerimos que você não tente transformar o valor se quiser trabalhar com valores padrão. É melhor lidar com os dados antes e depois do React Hook Form e do Zod, para evitar erros nessas transformações e transformações reversas.
Criando um input de moeda personalizado
Por fim, você pode abstrair os parâmetros e o componente Controller em um novo componente e personalizá-lo como desejar:
import { zodResolver } from '@hookform/resolvers/zod'
import {
Control,
Controller,
FieldError,
FieldErrorsImpl,
Merge,
SubmitHandler,
useForm
} from 'react-hook-form'
import { NumericFormat } from 'react-number-format'
import { z } from 'zod'
type DolarInputProps = {
name: string
error?: Merge<FieldError, FieldErrorsImpl<{}>> | null
control: Control<any, any> | undefined
}
const DolarInput = ({ name, error = null, control }: DolarInputProps) => {
return (
<>
<Controller
name={name}
control={control}
render={({ field: { ref, ...rest } }) => (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
getInputRef={ref}
{...rest}
/>
)}
/>
{!!error && <span>{error?.message}</span>}
</>
)
}
const formSchema = z.object({
currency: z.string()
})
type FormData = z.infer<typeof formSchema>
const Form = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormData>({
defaultValues: {
currency: '$ 10,000'
},
resolver: zodResolver(formSchema)
})
const onSubmit: SubmitHandler<FormData> = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<DolarInput name="currency" control={control} error={errors.currency} />
<button type="submit">Send</button>
</form>
)
}
export default Form
Melhorando a integração do React Hook Form (v7.44.0) com Zod e valores transformados/coagidos
Há três semanas, o react-hook-form lançou uma nova versão (v7.44.0) com a possibilidade de usar dois tipos diferentes em seu formulário, um para entrada de dados e outro para saída.
Assim, com esse novo recurso, podemos usar as transformações nativas do Zod, e nossos defaultValues continuarão recebendo o tipo de entrada, mas quando enviarmos o formulário, obteremos o tipo transformado.
Para fazer isso, em primeiro lugar, precisamos obter os tipos corretos, e o Zod possui dois métodos para inferir a entrada e a saída do seu esquema:
type FormInputData = z.input<typeof formSchema>
type FormOutputData = z.output<typeof formSchema>
// type FormData = z.infer<typeof formSchema>
Agora, com a API do react-hook-forma, podemos passar esses dois tipos no seu useForm, pois agora ele recebe três parâmetros dessa forma:
FormProviderProps<TFieldValues, TContext, TTransformedValues>
Por padrão, TContext é any, então manteremos o mesmo.
Outra dica útil é usar o Intl.NumberFormat para transformar nosso número transformado de volta em uma string:
import { zodResolver } from '@hookform/resolvers/zod'
import {
Control,
Controller,
FieldError,
FieldErrorsImpl,
Merge,
SubmitHandler,
useForm
} from 'react-hook-form'
import { NumericFormat } from 'react-number-format'
import { transformCurrencyStrinToNumber } from 'utils/transformCurrencyStrinToNumber'
import { z } from 'zod'
type DolarInputProps = {
name: string
error?: Merge<FieldError, FieldErrorsImpl<{}>> | null
control: Control<any, any> | undefined
}
const DolarInput = ({ name, error = null, control }: DolarInputProps) => {
return (
<>
<Controller
name={name}
control={control}
render={({ field: { ref, ...rest } }) => (
<NumericFormat
thousandSeparator=","
decimalSeparator="."
prefix="$ "
decimalScale={2}
getInputRef={ref}
{...rest}
/>
)}
/>
{!!error && <span>{error?.message}</span>}
</>
)
}
const formSchema = z.object({
currency: z.string().transform((val) => Number(val.replace(/[^\d.-]/g, '')))
})
type FormInputData = z.input<typeof formSchema>
type FormOutputData = z.output<typeof formSchema>
const Form = () => {
const {
handleSubmit,
control,
formState: { errors }
} = useForm<FormInputData, any, FormOutputData>({
defaultValues: {
currency: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(100000.56)
},
resolver: zodResolver(formSchema)
})
const onSubmit: SubmitHandler<FormOutputData> = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<DolarInput name="currency" control={control} error={errors.currency} />
<button type="submit">Send</button>
</form>
)
}
export default Form
Conclusão
Espero que este artigo seja útil para você. Foi necessário investir algum tempo para entender a maneira correta de usar o react-number-format com o react-hook-form, porque o exemplo que temos na documentação do react-hook-form não removeu a ref das props do campo e causou alguns erros, e também precisamos entender as limitações da integração do Zod com o react-hook-form. Mas, como mostramos, agora temos um novo recurso para nos ajudar e podemos usar as transformações nativas do Zod.
Se você tiver alguma dúvida ou sugestão sobre como podemos melhorar essas integrações, por favor, comente abaixo. Até logo!
Buy Me a Coffee
As a good programmer, I know you love a little coffee! So why don't you help me have a coffee while I produce this content for the whole community?💙
With just $3.00, you can help me, and more importantly, continue to encourage me to bring more completely free content to the whole community. You just need to click on the link below, I'm counting on your contribution 😉.
Buy Me a CoffeeSubscribe to our Newsletter!
By subscribing to our newsletter, you will be notified every time a new post appears. Don't miss this opportunity and stay up-to-date with all the news!
Subscribe!