Simplificando a criação de formulários no ReactJS usando Context e sem bibliotecas. (TS)
Olá pessoal, criei uma pequena estratégia de código pra simplificar a criação de formulários no react usando basicamente a ContextAPI do react. Lembrando que essa estrutura não é a solução perfeita (muito longe disso inclusive) numa aplicação mais robusta com certeza o ideal é utilizar alguma biblioteca madura e segura, mas para pequenos projetos essa estrutura pode ser bem interessante e agilizar muita coisa, sem mais delongas, vamos ao código.
OBS: Este post não teve nenhum contato com IA alguma, por isso, caso encontre algum erro de ortografia absurdo, ignore por favor (estou com sono).
Estrutura básica
Vamos começar criando uma pasta chamada Form
(geralmente dentro de uma pasta onde fica os componentes da aplicação, como por exemplo: src/components/Form
). dentro dela criaremos um index.tsx
que iremos utilizar como atalho para os componentes de formulário:
// index.tsx
export const Form = {}
Agora vamos iniciar nosso arquivo de contexto:
// Context.ts
export const FormContext = createContext(null) // temporariamente null
O container principal (onde ficará a tag form
):
// Container.tsx
import React, { FormHTMLAttributes, useState, FormEventHandler } from 'react'
const FormContainer: React.FC<FormHTMLAttributes<HTMLFormElement>> = (properties) => {
const [data, setData] = useState({})
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
}
return <form {...properties} onSubmit={handleSubmit} />
}
export default FormContainer
O nosso template de input:
// Input.tsx
import React, { InputHTMLAttributes } from 'react'
interface InputProperties extends InputHTMLAttributes<HTMLInputElement> {
label: string
id: string
}
const FormInput: React.FC<InputProperties> = ({ label, ...properties }) => {
return (
<div>
<label htmlFor={properties.id}>{label}:</label>
<input {...properties} />
</div>
)
}
export default FormInput
Particulamente eu gosto dessa estrutura de input onde eu já tenho um label, e por fim, antes de começar a juntar as coisas, vamos criar um arquivo para nossas interfaces que irão padronizar a conversa entre cada um dos componentes:
// interfaces.ts
import { ChangeEventHandler } from 'react'
export type InputChangeHandler = ChangeEventHandler<HTMLInputElement>
Conectando as partes
Agora finalmente vamos começar a conectar essas peças para que se comuniquem e agilizem nosso trabalho, essas são as etapas que o preenchimento de um formulário geralmente tem:
- Usuário digita num
input
, este capta a alteração e salva esses dados num estado através de um handler. - Após o preenchimento de todos os inputs, o usuário clica num botão de submit fazendo o
form
disparar o evento (que geralmente previnimos o comportamento padrão com um outro handler para evitar o reload). - Utilizamos o handler do form para recuperar os dados do estado e enviar para alguma API.
Como é possível observar, criamos um estado que será o estado global do formulário no arquivo Container.tsx
:
// Container.tsx
// ...
const [data, setData] = useState({})
// ...
Nosso objetivo é fazer com que nossos inputs atualizem esse estado. Vamos começar trabalhando no nosso Container.tsx
criando uma função genérica que consegue pegar o evento de um input qualquer e setar o valor dele no estado global do formulário aproveitando que definimos o atributo id
como obrigatório no nosso Input.tsx
:
// Container.tsx
// ...
import { InputChangeHandler } from './interfaces'
// ...
const [data, setData] = useState({})
const inputChangeHandler: InputChangeHandler = (event) => {
const { id, value } = event.target
setData({ ...data, [id]: value })
}
// ...
Agora que temos nossa função que irá atualizar nosso estado, precisamos deixa-la disponível para que inputs do nosso formulário possam utiliza-la. Faremos isso tornando essa função parte do nosso contexto:
// Context.ts
import { createContext } from 'react'
import { InputChangeHandler } from './interfaces'
interface FormContextProperties {
inputChangeHandler: InputChangeHandler
}
export const FormContext = createContext<FormContextProperties>({
inputChangeHandler: () => undefined, // valor inicial que será substituído
})
Nosso contexto está pronto e agora vamos adicionar o provider dele no nosso Container:
// Container.tsx
import { FormContext } from './Context'
// ...
return (
<FormContext.Provider value={{ inputChangeHandler }}>
<form {...properties} onSubmit={handleSubmit} />
</FormContext.Provider>
)
// ...
Agora nossos inputs que estiverem dentro desse provider, poderão usar esse contexto para recuperar o handler que tratará os eventos de alterações (onChange):
// Input.tsx
import React, { InputHTMLAttributes } from 'react'
import { FormContext } from './Context'
interface InputProperties extends InputHTMLAttributes<HTMLInputElement> {
label: string
id: string
}
const FormInput: React.FC<InputProperties> = ({ label, ...properties }) => {
const formContext = useContext(FormContext)
return (
<div>
<label htmlFor={properties.id}>{label}:</label>
<input onChange={formContext.inputChangeHandler} {...properties} />
</div>
)
}
export default FormInput
Com isso, finalizamos nosso componente de input, todos inputs criados com ele irão atualizar o estado global do formulário. Agora vamos lidar com a parte de submit, criando uma função que retorna nossos dados. Vamos começar criando uma interface para essa função:
// interfaces.ts
import { ChangeEventHandler } from 'react'
export type InputChangeHandler = ChangeEventHandler<HTMLInputElement>
export type FormDataHandler = (form: Record<string, string>) => void
Agora vamos implementar essa interface no nosso container:
// Container.tsx
// ...
import { FormDataHandler, InputChangeHandler } from './interfaces'
interface FormContainerProperties extends FormHTMLAttributes<HTMLFormElement> {
formData: FormDataHandler
}
const FormContainer: React.FC<FormContainerProperties> = ({ formData, ...properties }) => {
// ...
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
formData(form)
}
// ...
E para finalizar, vamos expor de forma simplificada as partes principais no nosso index:
// index.tsx
import FormContainer from './Container'
import FormInput from './Input'
export type { FormDataHandler } from './interfaces'
export const Form = {
Container: FormContainer,
Input: FormInput,
}
Considerações finais e como utilizar
Primeiramente, se você chegou até aqui, muito obrigado pela sua leitura, e você pode estar se perguntando "será mesmo que compensa tudo isso?" e nada melhor do que comparar uma estrutura tradicional à essa estrutura para validarmos:
// formulário tradicional
import React, { useState } from 'react'
const Form: React.FC = () => {
const [data, setData] = useState({
email: '',
senha: ''
})
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault()
// envio do estado 'data' para a API
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
required
value={data.email}
onChange={(e) => setData({ ...data, email: e.target.value })}
/>
<input
type="password"
minLength={8}
required
value={data.email}
onChange={(e) => setData({ ...data, password: e.target.value })}
/>
<button type="submit">login</button>
</form>
)
}
export default Form
Agora um exemplo de como usar o componente criado nesse post para criar formulários:
// form.tsx
import React from 'react'
import { Form, FormDataHandler } from './components/Form'
const Form: React.FC = () => {
const formDataHandler: FormDataHandler = (data) => {
// envio do estado 'data' para a API
}
return (
<Form.Container formData={formDataHandler}>
<Form.Input label="E-mail" id="email" type="email" required />
<Form.Input label="Password" id="password" type="password" minLength={8} required />
<button type="submit">login</button>
</Form.Container>
)
}
export default Form
A grande diferença é que toda a parte de estado já fica implementada automáticamente para cada formulário que criármos, dispensando useState
, value={...}
e onChange={(e) => ...}
. Podemos inclusive juntar todo este código num unico arquivo para facilitar replicar essa estrutura em diversos projetos.
Disclaimers
Reafirmo que essa estrutura não é a solução perfeita (muito muito longe disso inclusive) numa aplicação mais robusta o ideal é utilizar alguma biblioteca madura e segura, mas para pequenos projetos essa estrutura pode agilizar as coisas e ATENÇÃO, ela possui alguns pontos importantes que podem causar erros:
- Seu input fica escravo de um formulário, em casos como campos de busca que não necessáriamente vai ter um submit, seu input vai dar erro por não encontrar um contexto. Isso é facilmente resolvível extraindo seu input puro para fora do FormInput e utilizar o FormInput somente como wraper dele.
- Não sei exatamente por que alguem faria isso, mas não sei o que pode acontecer caso tente adicionar um formulário dentro de outro formulário, não sei como o React iria lidar com esse contexto duplicado (talvez utilizar o mais próximo do input, não sei).
- Também não sei pq alguem faria isso, mas como vimos, o atributo ID do input é utilizado como chave para armazenar o valor de cada input no estado, então criar inputs com IDs duplicados farão com que o estado de um input sobrescreva o do outro...
Caso decida testar essa estrutura e encontre novos problemas, compartilhe! Ficaria muito grato adicionar sua contribuição ao post. A todos que chegaram até aqui, meu muito obrigado e por favor, me adicione no GitHub e LinkedIn.