DEV Community

Marcoshsc
Marcoshsc

Posted on

Simplificando seu código React usando closures

Imagina o seguinte caso: você precisa fazer um formulário que lida com quatro diferentes campos: field1, field2, field3 e field4. Naturalmente, você cria o seu formulário seguindo essa estrutura:

const Formulario = () => {
  const [field1, setField1] = useState("");
  const [field2, setField2] = useState("");
  const [field3, setField3] = useState("");
  const [field4, setField4] = useState("");

  return (
    <form onSubmit={...}>
      <input label="Field 1" value={field1} onChange={(e) => setField1(e.target.value)} />
      <input label="Field 2" value={field2} onChange={(e) => setField2(e.target.value)} />
      <input label="Field 3" value={field3} onChange={(e) => setField3(e.target.value)} />
      <input label="Field 4" value={field4} onChange={(e) => setField4(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Temos um estado para cada campo, controlamos cada um dos inputs com seu respectivo valor, e colocamos no onChange um callback que muda o valor de acordo com o que o usuário digitou. Até aqui tudo bem, certo? Bom... esse código tem um problema claro à primeira vista: a lógica do callback onChange de todos os inputs está localizada dentro da função render, coisa que dependendo do tamanho do componente e do callback passado, pode dificultar a manutenção no futuro.
Pois bem: você se incomodou com isso e resolveu esse problema, extraindo todas as funções para callbacks no corpo do componente:

const Formulario = () => {
  const [field1, setField1] = useState("");
  const [field2, setField2] = useState("");
  const [field3, setField3] = useState("");
  const [field4, setField4] = useState("");

  const handleChangeField1 = (e) => {
    setField1(e.target.value)
  }

  const handleChangeField2 = (e) => {
    setField2(e.target.value)
  }

  const handleChangeField3 = (e) => {
    setField3(e.target.value)
  }

  const handleChangeField4 = (e) => {
    setField4(e.target.value)
  }

  return (
    <form onSubmit={...}>
      <input label="Field 1" value={field1} onChange={handleChangeField1} />
      <input label="Field 2" value={field2} onChange={handleChangeField2} />
      <input label="Field 3" value={field3} onChange={handleChangeField3} />
      <input label="Field 4" value={field4} onChange={handleChangeField4} />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Resolvido agora, certo? Bom, melhorou um pouco porém ainda temos um problema: se tivermos 10 campos, precisaremos declarar 10 callbacks? Ora, analisando os quatro callbacks criados, podemos perceber que todos eles fazem tarefas parecidas: receber um evento e setar o valor do estado com e.target.value.
E como poderíamos resolver este problema? Afinal, a função onChange está esperando um callback neste exato formato que criamos. Será que existe uma maneira de criar um callback neste formato, que se adapta a cada diferente estado do input? Bom, para isso podemos utilizar uma estrutura no javascript chamada de closures.

O que são closures?

Um closure é um conceito que se refere à uma função que é criada dentro de outra função, com acesso ao seu contexto léxico. Para simplificar, confere esse exemplo:

const outer = (name) => {
  const inner = () => {
    console.log(name)
  }
  inner()
}

Enter fullscreen mode Exit fullscreen mode

Perceba que, dentro da função outer, é declarada uma função inner, sem parâmetros, porém que consegue acessar o parâmetro name da função outer (contexto léxico), e printar o valor na sequência. Após sua criação, a função inner pode ser utilizada normalmente, da forma como foi criada. Por exemplo, caso fosse executado outer('marcos'), seria imprimido a string 'marcos' no console.
Closures também podem ser usados como geradores de funções mais simples:

const getPropertySetter = (propertyName) => {
  const setProperty = (obj, value) => {
    obj[propertyName] = value 
  }
  return setProperty
}
Enter fullscreen mode Exit fullscreen mode

Com esse closure, posso simplificar o processo de modificar uma propriedade em um objeto, por exemplo. Observe que a função externa recebe somente o nome da propriedade, e retorna uma função nova, que recebe um objeto e o valor da propriedade, e faz a atribuição desse valor à propriedade cujo nome foi informado na função mais externa, no objeto recebido. Essa função poderia ser utilizada da seguinte maneira:

const setName = getPropertySetter("name");
const setAge = getPropertySetter("age");
const obj = {};
setName(obj, "marcos");
setAge(obj, 22);
console.log(obj);
// output = { name: 'marcos', age: 22 }
Enter fullscreen mode Exit fullscreen mode

Perceba que um closure te permite criar funções que geram outras funções, que resolvem problemas de uma maneira mais simples ou mais conveniente.

De volta ao problema

Agora que você sabe o que é um closure, como podemos utilizar esse conceito para melhorar o código do formulário? Vamos identificar as semelhanças com os exemplos de closure:

  • Precisamos passar um callback para o onChange, com um parâmetro que é o evento
  • Porém, algo que muda para cada input é a função setter do estado.
  • Logo, podemos criar um closure onde a função externa recebe o setter, e retornar então uma função interna com o formato que desejamos, que faz a tarefa comum:
const handleChange = (stateSetter) => {
    const setFieldValue = (e) => {
      stateSetter(e.target.value) 
    }
    return setFieldValue
  }
Enter fullscreen mode Exit fullscreen mode

E podemos então reutilizar esse closure em todos os nossos inputs:

return (
    <form onSubmit={...}>
      <input label="Field 1" value={field1} onChange={handleChange(setField1)} />
      <input label="Field 2" value={field2} onChange={handleChange(setField2)} />
      <input label="Field 3" value={field3} onChange={handleChange(setField3)} />
      <input label="Field 4" value={field4} onChange={handleChange(setField4)} />
      <button type="submit">Submit</button>
    </form>
  );
Enter fullscreen mode Exit fullscreen mode

Perceba que agora, temos a lógica separada da renderização, e o número de campos pode aumentar infinitamente, que não precisaremos escrever novos callbacks, somente reutilizar nosso closure. Ao final, nosso componente fica da seguinte maneira:

const Formulario = () => {
  const [field1, setField1] = useState("");
  const [field2, setField2] = useState("");
  const [field3, setField3] = useState("");
  const [field4, setField4] = useState("");

  const handleChange = (stateSetter) => {
    const setFieldValue = (e) => {
      stateSetter(e.target.value) 
    }
    return setFieldValue
  }

  return (
    <form onSubmit={...}>
      <input label="Field 1" value={field1} onChange={handleChange(setField1)} />
      <input label="Field 2" value={field2} onChange={handleChange(setField2)} />
      <input label="Field 3" value={field3} onChange={handleChange(setField3)} />
      <input label="Field 4" value={field4} onChange={handleChange(setField4)} />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

Pronto! Temos um componente muito mais limpo e escalável.

Conclusão

Closures são muito importantes no javascript, especialmente no React. Podem te ajudar e muito a deixar seu código mais limpo e simples, e facilitar a reutilização de código. Aqui eu dei somente um exemplo de como poderiam ser utilizados no React, porém eles são aplicáveis a qualquer lógica semelhante ao que mostrei aqui.

E ai, já sabia do que se tratavam os closures? Aprendeu algo novo? Deixa ai nos comentários! Muito obrigado pela leitura, e até o próximo post!

Top comments (0)