Reagir o desenvolvimento do formulário. KISS, YAGNI, DRY princípios na prática

Olá, neste tutorial, veremos como desenvolver uma forma muito simples, porém controlada, no React, com foco na qualidade do código.

Ao desenvolver nosso formulário, seguiremos os princípios de "KISS", "YAGNI", "DRY". Para concluir com êxito este tutorial, você não precisa conhecer esses princípios, explicarei ao longo do caminho. No entanto, acredito que você tenha um bom domínio do javascript moderno e possa pensar em React .



Estrutura do tutorial:







Para a causa! Escrevendo um formulário simples usando o KISS e o YAGNI


Então, vamos imaginar que temos uma tarefa para implementar um formulário de autorização:
Copiar código
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



Começamos nosso desenvolvimento analisando os princípios do KISS e YAGNI, esquecendo temporariamente o restante dos princípios.

BEIJO - "Deixe o código simples e burro." Eu acho que você está familiarizado com o conceito de código simples. Mas o que significa código "burro"? No meu entendimento, este é um código que resolve o problema usando o número mínimo de abstrações, enquanto o aninhamento dessas abstrações entre si também é mínimo.

YAGNI - "Você não precisa disso". O código deve poder fazer apenas o que está escrito. Não criamos nenhuma funcionalidade que possa ser necessária posteriormente ou que melhore o aplicativo em nossa opinião. Fazemos apenas o necessário especificamente para a implementação da tarefa.

Vamos seguir rigorosamente esses princípios, mas também considerar:

  • initialDatae onSubmitfor LogInFormvem do topo (essa é uma técnica útil, especialmente quando o formulário deve ser capaz de processar createe updateao mesmo tempo)
  • deve ter para cada campo label

Além disso, vamos perder a estilização, pois ela não é interessante e válida, pois esse é um tópico para um tutorial separado.

Por favor, implemente o formulário, seguindo os princípios descritos acima.

Implementação do meu formulário
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Enter your nickname
        <input
          value={logInData.nickname}
          onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
        />
      </label>
      <label>
        Enter your email
        <input
          type="email"
          value={logInData.email}
          onChange={e => setLogInData({ ...logInData, email: e.target.value })}
        />
      </label>
      <label>
        Enter your password
        <input
          type="password"
          value={logInData.password}
          onChange={e => setLogInData({ ...logInData, password: e.target.value })}
        />
      </label>
      <button>Submit</button>
    </form>
  );
};
    






Sua decisão, provavelmente, é um pouco diferente, porque cada desenvolvedor pensa de maneira diferente. Você pode criar seu próprio estado para cada campo ou colocar as funções do manipulador em uma variável separada; em qualquer caso, isso não é importante.

Mas se você criou imediatamente uma função de manipulador para todos os campos, essa não é mais a solução mais "burra" para o problema. E nosso objetivo, neste estágio, é criar o código mais simples e mais "burro", para que mais tarde possamos analisar a imagem inteira e selecionar a melhor abstração.

Se você tem exatamente o mesmo código, isso é legal e significa que nosso pensamento converge!

Além disso, trabalharemos com esse código. É simples, mas ainda longe do ideal.




Refatoração e DRY


É hora de lidar com o princípio DRY.

Redação seca e simplificada - "não duplique seu código". O princípio parece simples, mas tem um problema: para se livrar da duplicação de código, você precisa criar abstrações. Se essas abstrações não forem boas o suficiente, violaremos o princípio do KISS.

Também é importante entender que o DRY não é necessário para escrever código mais rapidamente. Sua tarefa é simplificar a leitura e o suporte da nossa solução. Portanto, não se apresse em criar abstrações imediatamente. É melhor fazer uma implementação simples de alguma parte do código e depois analisar quais abstrações você precisa criar para simplificar a leitura do código e reduzir o número de locais para alterações quando necessário.

Lista de verificação da abstração correta:
  • o nome da abstração é totalmente consistente com seu objetivo
  • abstração executa uma tarefa específica e compreensível
  • a leitura do código do qual a abstração foi extraída melhorou

Então, vamos começar a refatorar.
E pronunciamos código duplicado:
Copiar código
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Este código duplica uma composição de 2 elementos: label, input. Vamos fundi-los em uma nova abstração InputField:
Copiar código
  <label>
    Enter your email
    <input
      type="email"
      value={logInData.email}
      onChange={e => setLogInData({ ...logInData, email: e.target.value })}
    />
  </label>
  <label>
    Enter your password
    <input
      type="password"
      value={logInData.password}
      onChange={e => setLogInData({ ...logInData, password: e.target.value })}
    />
  </label>



Agora o nosso LogInFormé assim:
Copiar código
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={e => setLogInData({ ...logInData, nickname: e.target.value })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={e => setLogInData({ ...logInData, email: e.target.value })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={e => setLogInData({ ...logInData, password: e.target.value })}
      />
      <button>Submit</button>
    </form>
  );
};



Ler ficou mais fácil. O nome da abstração corresponde à tarefa que ela resolve. O objetivo do componente é óbvio. O código se tornou menor. Então, estamos indo na direção certa!

Agora está claro que a InputField.onChangelógica está duplicada.
O que acontece lá pode ser dividido em 2 etapas:

Copiar código
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



A primeira função descreve os detalhes de como obter o valor do evento input. Temos uma escolha de 2 abstrações nas quais podemos armazenar essa lógica: InputFielde LogInForm.

Para determinar corretamente a quais abstrações precisamos referenciar nosso código, teremos que recorrer à formulação completa do princípio DRY: "Cada parte do conhecimento deve ter uma representação única, consistente e autorizada dentro do sistema".

Parte do conhecimento no exemplo concreto é saber como obter o valor do evento em input. Se armazenarmos esse conhecimento no nosso LogInForm, é óbvio que, ao usar nossoInputFieldde outra forma, teremos que duplicar nosso conhecimento ou trazê-lo para uma abstração separada e usá-lo a partir daí. E com base no princípio do KISS, devemos ter o número mínimo possível de abstrações. De fato, por que precisamos criar outra abstração se podemos simplesmente colocar essa lógica na nossa InputFielde o código externo não saberá nada sobre como a entrada funciona por dentro InputField. Ele simplesmente assumirá um significado pronto, o mesmo que passar para dentro.

Se você está confuso de que possa precisar de um evento no futuro, lembre-se do princípio YAGNI. Você sempre pode adicionar um suporte adicional onChangeEventao nosso componente InputField.

Até lá, InputFieldficará assim:
Copiar código
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



Assim, a uniformidade do tipo é observada durante a entrada e a saída no componente e a verdadeira natureza do que está acontecendo no código externo fica oculta. Se, no futuro, precisarmos de outro componente da interface do usuário, por exemplo, marque a caixa de seleção ou selecione, então também manteremos a uniformidade de tipo na entrada e na saída.

Essa abordagem nos oferece flexibilidade adicional, pois nosso formulário pode funcionar com qualquer fonte de entrada e saída sem a necessidade de produzir manipuladores exclusivos adicionais para cada uma.

Esse truque heurístico é incorporado por padrão em muitas estruturas. Por exemplo, esta é a idéia principal v-modelem Vueque muitas pessoas adoram por sua facilidade de trabalhar com formulários.

Vamos voltar aos negócios, atualizar nosso componente LogInFormde acordo com as alterações em InputField:
Copiar código
const LogInForm = ({ initialData, onSubmit }) => {
  const [logInData, setLogInData] = useState(initialData);

  const handleSubmit = e => {
    e.preventDefault();
    onSubmit(logInData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={logInData.nickname}
        onChange={nickname => setLogInData({ ...logInData, nickname })}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={logInData.email}
        onChange={email => setLogInData({ ...logInData, email })}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={logInData.password}
        onChange={password => setLogInData({ ...logInData, password })}
      />
      <button>Submit</button>
    </form>
  );
};



Já parece muito bom, mas podemos fazer ainda melhor!

Callbackque é passado onChangesempre faz a mesma coisa. Somente a chave é alterada: senha, email, apelido. Assim, podemos substituí-lo com uma chamada para a função: handleChange('password').

Vamos implementar esta função:
Copiar código
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



Como você pode ver, a função recebe o argumento e o armazena no fechamento do manipulador, e o manipulador retorna imediatamente ao código externo. As funções que parecem receber argumentos um de cada vez para uma chamada também são chamadas de curry.

Vamos dar uma olhada no código resultante:
Copiar código
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    const handleChange = fieldName => fieldValue => {
      setLogInData({
        ...logInData,
        [fieldName]: fieldValue,
      });
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };
  
  // InputField.js
  const InputField = ({ type, label, value, onChange }) => (
    <label>
      {label}
      <input type={type} value={value} onChange={e => onChange(e.target.value)} />
    </label>
  );



Este código é uma implementação curta, concisa e moderadamente declarativa da tarefa. Refatoração adicional no contexto da tarefa, na minha opinião, não faz sentido.




O que mais pode ser feito?



Se você tiver muitos formulários no projeto, poderá colocar o cálculo handleChange em um gancho separado useFieldChange:
Copiar código
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



Como essa é uma função pura (ou seja, na primeira vez em que é chamada porque sempre retorna a mesma função), ela não precisa ser um gancho. Mas o gancho parece conceitualmente uma solução mais correta e natural para o React.

Você também pode adicionar suporte callbackno local fieldValuepara replicar completamente o comportamento usual setStatede React:
Copiar código
  const isFunc = val => typeof val === "function";

  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: isFunc(fieldValue) ? fieldValue(state[fieldName]) : fieldValue,
    }));
  };



Um exemplo de uso com nosso formulário:
Copiar código
  const LogInForm = ({ initialData, onSubmit }) => {
    const [logInData, setLogInData] = useState(initialData);
    const handleChange = useFieldChange(setLogInData);
  
    const handleSubmit = e => {
      e.preventDefault();
      onSubmit(logInData);
    };
  
    return (
      <form onSubmit={handleSubmit}>
        <InputField
          label="Enter your nickname"
          value={logInData.nickname}
          onChange={handleChange('nickname')}
        />
        <InputField
          type="email"
          label="Enter your email"
          value={logInData.email}
          onChange={handleChange('email')}
        />
        <InputField
          type="password"
          label="Enter your password"
          value={logInData.password}
          onChange={handleChange('password')}
        />
        <button>Submit</button>
      </form>
    );
  };



Mas se seu aplicativo tiver apenas um formulário, você não precisará fazer tudo isso! Porque contradiz o princípio YAGNI, após o qual não devemos fazer o que não precisamos para resolver um problema específico. Se você tiver apenas uma forma, os benefícios reais de tais movimentos não serão suficientes. Afinal, reduzimos o código do nosso componente em apenas três linhas, mas introduzimos uma abstração adicional, ocultando uma certa lógica da forma, que é melhor manter na superfície.




Ah não! Não faça isso, por favor!



Formulários de configuração em forma


As configurações bloqueadas são como uma configuração do webpack, apenas para o formulário.

Melhor com um exemplo, veja este código:
Copiar código
  const Form = () => (
    <form onSubmit={handleSubmit}>
      <InputField
        label="Enter your nickname"
        value={state.nickname}
        onChange={handleChange('nickname')}
      />
      <InputField
        type="email"
        label="Enter your email"
        value={state.email}
        onChange={handleChange('email')}
      />
      <InputField
        type="password"
        label="Enter your password"
        value={state.password}
        onChange={handleChange('password')}
      />
      <button>Submit</button>
    </form>
  );



Pode parecer para alguns que o código está duplicado aqui, porque chamamos o mesmo componente InputField, passando o mesmo rótulo, valor e parâmetros onChange lá. E eles começam a usar o próprio código para DRY-yav para evitar duplicação imaginária.
Muitas vezes eles fazem isso assim:
Copiar código
const fields = [
  {
    name: 'nickname',
    label: 'Enter your nickname',
  },
  {
    type: 'email',
    name: 'email',
    label: 'Enter your email',
  },
  {
    type: 'password',
    name: 'password',
    label: 'Enter your password',
  },
];

const Form = () => (
  <form onSubmit={handleSubmit}>
    {fields.map(({ type, name, label }) => (
      <InputField
        type={type}
        label={label}
        value={state[name]}
        onChange={handleChange(name)}
      />
    ))}
    <button>Submit</button>
  </form>
);



Como resultado, com 17 linhas de código jsx, obtemos 16 linhas da configuração. Bravo! Isto é o que eu entendo DRY. Se tivermos 100 dessas entradas aqui, obteremos 605 e 506 linhas, respectivamente.

Mas, como resultado, obtivemos um código mais complexo, violando o princípio do KISS. Afinal, agora ele consiste em 2 abstrações: campos e um algoritmo (sim, um algoritmo também é uma abstração), que o transforma em uma árvore de elementos React. Ao ler este código, teremos que pular constantemente entre essas abstrações.

Mas o maior problema com esse código é seu suporte e extensão.
Imagine o que acontecerá com ele se:
  • mais alguns tipos de campos com diferentes propriedades possíveis serão adicionados
  • campos são renderizados ou não renderizados com base na entrada de outros campos
  • alguns campos requerem algum processamento adicional ao alterar
  • valores em selects dependem de selects anteriores

Esta lista pode ser continuada por um longo tempo ...

Para implementar isso, você sempre precisará armazenar algumas funções em sua configuração, e o código que as renderiza gradualmente se transformará em Frankenstein, pegando vários adereços diferentes e jogando-os sobre componentes diferentes.

O código que foi escrito antes, com base na composição dos componentes, se expandirá silenciosamente, mudará conforme você desejar, sem se tornar muito complicado devido à falta de abstração intermediária - um algoritmo que constrói uma árvore de elementos React a partir da nossa configuração. Este código não é duplicado. Ele segue apenas um determinado padrão de design e, portanto, parece "padronizado".

Otimização inútil


Depois que os ganchos apareceram no React, houve uma tendência de envolver todos os manipuladores e componentes indiscriminadamente em useCallbacke memo. Por favor não faça isso! Esses ganchos não foram fornecidos pelos desenvolvedores do React porque o React é lento e tudo precisa ser otimizado. Eles oferecem espaço para otimizar seu aplicativo, caso você encontre problemas de desempenho. E mesmo se você encontrar esses problemas, não precisará envolver todo o projeto em memoe useCallback. Use o Profiler para identificar problemas e só então memorize no lugar certo.

Memoizar sempre tornará seu código mais complicado, mas nem sempre mais produtivo.

Vejamos o código de um projeto real. Compare como é a função com useCallbacke sem:
Copiar código
  const applyFilters = useCallback(() => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  }, [selectedMetricsStatus, onApplyFilterClick]);

  const applyFilters = () => {
    const newSelectedMetrics = Object.keys(selectedMetricsStatus).filter(
      metric => selectedMetricsStatus[metric],
    );
    onApplyFilterClick(newSelectedMetrics);
  };



A legibilidade do código aumentou claramente após a remoção do wrapper, porque o código ideal é a sua falta.

O desempenho deste código não aumentou, porque esta função é usada assim:
Copiar código
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



Onde RightFooterButton- é apenas styled.buttona partir styled-components, que irá atualizar muito rapidamente. Mas o consumo de memória aumentar nossa aplicação, porque Reagir manterá sempre na memória selectedMetricsStatus, onApplyFilterClicke a versão da função applyFilters, relevantes para essas dependências.

Se esses argumentos não forem suficientes para você, leia o artigo que discute esse tópico mais amplamente.




achados


  • Os formulários de reação são fáceis. Os problemas com eles surgem devido aos próprios desenvolvedores e à documentação do React, na qual este tópico não é divulgado em detalhes suficientes.
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


Inicialmente, planejei escrever sobre a validação declarativa de formulários aninhados complexos. Mas, no final, decidi preparar o público para isso, para que a imagem fosse o mais completa possível.
O tutorial a seguir será sobre a validação do formulário simples implementado neste tutorial.
Muito obrigado a todos que leram até o fim!

Não importa como você escreve o código ou o que faz, o principal é aproveitá-lo.

All Articles