Esta é a segunda parte do assunto de formulário sem bibliotecas no react, na primeira escrevi um exemplo de formulário sem biblioteca que funciona muito bem com validação onChange e hook de validação, mas se você leu a postagem anterior até o final deve ter percebido que o exemplo apesar de ser bom, não era tão performático por ter varias renderizações ao digitar algo nos campos.
Primeira parte do assunto: https://dev.to/jucian0/reactjs-formularios-55mo
E nesta postagem irei abordar mais o tema de desempenho, tentarei responder como criar um componente de formulário mais performático, isto é reduzir ao máximo o número de renderizações da tela, mas sem perder a comodidade de validação onChange.
Link do repositório: https://github.com/Jucian0/react-form-controlled
Se você não leu a postagem anterior sugiro uma leitura rápida para entender como a aplicação funciona.
Novo componente FormDebounce
No arquivo, App.jsx tem um comentário:
import React from 'react';
import FormControlled from './Components/FormControlled'
function App() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-lg-6 col-md-6">
{/* vai receber segundo abordagem */}
</div>
<div className="col-lg-6 col-md-6">
<FormControlled />
</div>
</div>
</div>
);
}
export default App;
Esse comentário vai dar lugar ao novo componente de formulário otimizado. Como os componentes são muito parecidos vou fazer uma cópia do componente FormControlled com o nome FormDebounce, e se você já tem uma certa experiência já deve ter matado a charada ao ler o nome do componente, mas tudo bem continue comigo até o final.
Um detalhe importante é alterar a propriedade de value para defaultValue nos Inputs do formulário, como não estou mais usando um formulário controllado é necessário adicionar uma propriedade que da valor inicial para o componente.
<Input
name="name"
onChange={e => setInput({ name: e.target.value })}
label="Name"
error={errors.name}
defaultValue={form.name}
/>
Você pode ler mais em: https://reactjs.org/docs/uncontrolled-components.html#default-values
Agora vou mudar o componente App.jsx para receber o novo componente de formulário:
function App() {
return (
<div className="container-fluid">
<div className="row">
<div className="col-lg-6 col-md-6">
<FormDebounce />
</div>
<div className="col-lg-6 col-md-6">
<FormControlled />
</div>
</div>
</div>
);
}
Criando a função debounce
Debounce é o nome dado a uma função de ordem superior
Mas o que é uma função de ordem superior?
Função de ordem superior é uma função que retorna outra função
Então debounce é uma função que retorna outra função, e essa função retornada executa outra função que é passada por parâmetro para a função debounce com uma espera de tempo que é passado por parâmetro para a função debounce também.
const funcCallBack = ()=>{}//sou executada dentro de "funcA"'
const time = 3000//determino o tempo de espera que a "funcCallBack" é executada
const funcA = debounce(
funcCallBack,
time
)
funcA()//passo meus parâmetros para "funcCallBack"
De forma simples esse é o funcionamento de uma função debounce, agora abaixo veja como fica o código dela:
export const debounce = (fn, wait, immediate) => {
let timeout
return (...args) => {
const context = this
const later = () => {
timeout = null
if (!immediate) fn.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) {
fn.apply(context, args)
}
}
}
O motivo de criar essa função é que quero reduzir o número de vezes que o estado do componente é atualizado e dessa forma diminuir as renderizações desnecessárias.
Adicionando debounce ao componente de Input
Vou usar a função debounce atrelada ao evento onChange do input e adicionar um tempo de espera de 500 milissegundos. Assim garanto que o estado do componente FormDebounce só muda depois de 500 milissegundos que paro de digitar, em uma situação normal o componente seria renderizado a cada caractere que eu digitasse no input, veja como fica o código:
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { debounce } from '../Debounce';
const Input = ({ error, label, onChange, ...rest }) => {
const [touched, setTouched] = useState(false)
const inputRef = useRef(null)
const debounceInput = useCallback(debounce(onChange, 500), [debounce])
const blurInput = useCallback(() => setTouched(true), [setTouched])
useEffect(() => {
inputRef.current.addEventListener('input', debounceInput)
inputRef.current.addEventListener('blur', blurInput)
}, [blurInput, debounceInput, inputRef])
return (
<>
<label htmlFor={rest.name}>{label}</label>
<input className="form-control" {...rest} ref={inputRef} />
<span className="text-danger">{touched && error}</span>
</>
);
}
export default Input
Abaixo explico cada pare do código.
Linha 5 — Continuo usando a bordagem de useState para salvar o estado de touched do input.
Linha 6 — Usando o hook useRef crio uma referência para o input, com ela pego os eventos input e blur e passo as devidas funções de callback*.*
Linha 7 — Faço uso da função debounce e passo para ela a função onChange recebida das propriedades do componente Input, e também adiciono o valor de 500 milissegundos.
Linha 8 — Nesta parte crio uma função que executa o setTouched que no que lhe concerne é responsável por atualizar o estado de touched.
-
Linhas 10 a 13 — Usando o useEffect e a referência inputRef adiciono as funções responsáveis por cada evento no seu devido lugar. Em dependências adiciono debounceInput, blurInput e inputRef como dependências, como descrito em uma postagem de Dan Abramov sobre useEffect. https://overreacted.io/a-complete-guide-to-useeffect/
A common mistake is to think functions shouldn’t be dependencies.
Linhas 15 a 22 — Retorno o template do componente, não houve muitas mudanças nessa parte do código.
Um detalhe importante para ser explicado é o uso do hook useCallback envolvendo às duas funções que estão sendo usadas dentro do useEffect. Bem useCallback memoriza a função que recebe como argumento e devolve sempre a mesma função a menos que uma de suas dependências mude, e como descrito acima na citação de Dan Abramov as funções dentro de um componente mudam sempre que um componente é renderizado, então quando uso o useCallback garanto que a função não seja recriada desde que as dependências de useCallback não mudem.
Melhorando o hook useValidation
Na versão da etapa anterior o hook useValidation retornava apenas um objeto de erros, vou adicionar uma nova propriedade ao objeto de retorno o isValid, uma propriedade booliana. Além disso, algumas melhorias no uso de useEffect dentro do hook, veja como fica o código com as mudanças:
import { useState, useEffect, useCallback } from 'react'
import { ValidationError } from "yup"
const useValidation = (values, schema) => {
const [errors, setErrors] = useState({})
const [isValid, setIsValid] = useState(false)
const validate = useCallback(async () => {
try {
await schema.validate(values, { abortEarly: false })
setErrors({})
setIsValid(true)
} catch (e) {
if (e instanceof ValidationError) {
const errors = {}
e.inner.forEach((key) => {
errors[key.path] = key.message
})
setErrors(errors)
setIsValid(false)
}
}
}, [schema, values])
useEffect(() => {
validate()
}, [validate])
return { errors, isValid }
}
export default useValidation
Algumas explicações das mudanças que fiz no código:
Linhas 5 e 6 — useState esta criando estados para as propriedades que o hook devolve, isValid e errors.
Linhas 8 a 24 — Adicionada função validate, esta função deve tentar passar os valores do form pelo crivo das validações que foram escritas na etapa anterior, se ela obtiver sucesso eu altero o estado de erros com um objeto vazio, mas se houver um erro de validação o fluxo entra no bloco catch esse bloco não pega apenas erros oriundos da promise de validação por isso faço uma validação se o erro é uma instância de ValidationError caso a validação seja afirmativo o atualizo o estado de errors. É importante notar que o processo de validação é assíncrono por isso uso um async await** **nessa função. Esta função esta envolta de useCallback isso vai evitar que a mesma seja recriada sem necessidade, somente quando as dependências mudarem a ela é recriada.
Linhas 25 a 27 — passo a função validate com dependência, como ela muda quando schema ou values mudam a mesma vai continuar sendo executada quando necessário.
As outras partes
O schema de validação continua inalterado, e componente form apenas pego a nova propriedade isValid do useValidate e adiciono no botão de submit.
<div className="form-group">
<button
type="button" className="btn btn-
primary" disabled={!isValid}>
Submit
</button>
</div>
Comparando os formulários
Vou comparar a quantidade de renderizações usando a extensão do react para desenvolvimento que pode ser encontrado para Chrome e Firefox. No navegador vou clicar f12 e selecionar profiler e Start Profiling.
Resultados para FormDebounce:
Resultados para FormControlled:
No primeiro exemplo temos três renderizações de componente, no segundo temos 13 renderizações, é uma diferença bem grande.
Com este exemplo não estou dizendo que essa deve ser a abordagem ideal, pode ser que em diversos casos isso não faça sentido, mas em outros, deve melhorar muito o desempenho da aplicação.
Link do repositório github: https://github.com/Jucian0/react-form-debounce
Funcionando no codesandbox
https://codesandbox.io/s/github/Jucian0/react-form-debounce/tree/master/?from-embed
Se você chegou até aqui espero que a postagem tenha servido para alguma coisa.
Uma Alternativa
Se você acredita que ainda precisa de uma biblioteca de formulários não deixem de conferir um projeto que estou desenvolvendo:
Github do projeto https://github.com/Jucian0/react-data-forms
Top comments (0)