DEV Community

loading...
Cover image for React: Formulários parte 2

React: Formulários parte 2

jucian0 profile image Juciano Updated on ・7 min read

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;
Enter fullscreen mode Exit fullscreen mode

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}
 />
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 como 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"
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 por parâmetro 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Docs: https://www.react-data-forms.org/

Discussion

pic
Editor guide