Reaccionar forma de desarrollo. Principios de KISS, YAGNI, DRY en la pr谩ctica

Hola, en este tutorial veremos c贸mo desarrollar una forma muy simple pero controlada en React, centr谩ndonos en la calidad del c贸digo.

Al desarrollar nuestro formulario, seguiremos los principios de "BESO", "YAGNI", "SECO". Para completar con 茅xito este tutorial no necesita conocer estos principios, los explicar茅 a lo largo del camino. Sin embargo, creo que tiene un buen dominio del javascript moderno y puede pensar en React .



Estructura tutorial







A la causa! Escribir un formulario simple usando KISS y YAGNI


Entonces, imaginemos que tenemos la tarea de implementar un formulario de autorizaci贸n:
C贸digo de copia
const logInData = {
  nickname: 'Vasya',
  email: 'pupkin@gmail.com',
  password: 'Reac5$$$',
};



Comenzamos nuestro desarrollo analizando los principios de KISS y YAGNI, olvidando temporalmente el resto de los principios.

BESO - "Deje el c贸digo simple y tonto". Creo que est谩 familiarizado con el concepto de c贸digo simple. Pero, 驴qu茅 significa el c贸digo "tonto"? Seg煤n tengo entendido, este es un c贸digo que resuelve el problema utilizando el n煤mero m铆nimo de abstracciones, mientras que el anidamiento de estas abstracciones entre s铆 tambi茅n es m铆nimo.

YAGNI: "No lo necesitar谩s". El c贸digo debe poder hacer solo para lo que est谩 escrito. No creamos ninguna funcionalidad que pueda ser necesaria m谩s adelante o que mejore la aplicaci贸n en nuestra opini贸n. Hacemos solo lo que se necesita espec铆ficamente para la implementaci贸n de la tarea.

Sigamos estrictamente estos principios, pero tambi茅n consideremos:

  • initialDatay onSubmitpara LogInFormviene de la parte superior (esta es una t茅cnica 煤til, especialmente cuando el formulario debe ser capaz de procesar createy updateal mismo tiempo)
  • debe tener para cada campo label

Adem谩s, echemos de menos la estilizaci贸n, ya que no es interesante para nosotros y es v谩lida, ya que este es un tema para un tutorial por separado.

Implemente el formulario usted mismo, siguiendo los principios descritos anteriormente.

Mi forma de implementaci贸n
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>
  );
};
    






Su decisi贸n, muy probablemente, es algo diferente, porque cada desarrollador piensa de manera diferente. Puede crear su propio estado para cada campo o colocar funciones de controlador en una variable separada, en cualquier caso, esto no es importante.

Pero si cre贸 inmediatamente una funci贸n de controlador para todos los campos, entonces esta ya no es la soluci贸n "m谩s tonta" del problema. Y nuestro objetivo en esta etapa es crear el c贸digo m谩s simple y "tonto" para que luego podamos ver la imagen completa y seleccionar la mejor abstracci贸n.

Si tiene exactamente el mismo c贸digo, 隆esto es genial y significa que nuestro pensamiento converge!

Adem谩s trabajaremos con este c贸digo. Es simple, pero a煤n lejos de ser ideal.




Refactorizaci贸n y SECO


Es hora de lidiar con el principio DRY.

DRY, redacci贸n simplificada: "no duplique su c贸digo". El principio parece simple, pero tiene un inconveniente: para deshacerse de la duplicaci贸n de c贸digo, debe crear abstracciones. Si estas abstracciones no son lo suficientemente buenas, violaremos el principio KISS.

Tambi茅n es importante comprender que DRY no es necesario para escribir c贸digo m谩s r谩pido. Su tarea es simplificar la lectura y el soporte de nuestra soluci贸n. Por lo tanto, no se apresure a crear abstracciones de inmediato. Es mejor hacer una implementaci贸n simple de alguna parte del c贸digo, y luego analizar qu茅 abstracciones necesita crear para simplificar la lectura del c贸digo y reducir el n煤mero de lugares para los cambios cuando sean necesarios.

Lista de verificaci贸n de la abstracci贸n correcta:
  • el nombre de la abstracci贸n es totalmente consistente con su prop贸sito
  • la abstracci贸n realiza una tarea espec铆fica y comprensible
  • La lectura del c贸digo del que se extrajo la abstracci贸n mejor贸

Entonces, vamos a refactorizar.
Y hemos pronunciado c贸digo duplicado:
C贸digo de copia
  <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 una composici贸n de 2 elementos: label, input. Fusion茅moslos en una nueva abstracci贸n InputField:
C贸digo de copia
  <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>



Ahora el nuestro se LogInFormve as铆:
C贸digo de copia
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>
  );
};



Leer se ha vuelto m谩s f谩cil. El nombre de la abstracci贸n corresponde a la tarea que resuelve. El prop贸sito del componente es obvio. El c贸digo se ha vuelto menos. 隆Entonces vamos en la direcci贸n correcta!

Ahora est谩 claro que la InputField.onChangel贸gica est谩 duplicada.
Lo que sucede all铆 se puede dividir en 2 etapas:

C贸digo de copia
const stage1 = e => e.target.value;
const stage2 = password => setLogInData({ ...logInData, password });



La primera funci贸n describe los detalles para obtener el valor del evento input. Tenemos la opci贸n de 2 abstracciones en las que podemos almacenar esta l贸gica: InputFieldy LogInForm.

Para determinar correctamente a cu谩l de las abstracciones necesitamos referir nuestro c贸digo, tendremos que recurrir a la formulaci贸n completa del principio DRY: "Cada parte del conocimiento debe tener una representaci贸n 煤nica, consistente y autorizada dentro del sistema".

Parte del conocimiento en el ejemplo concreto es saber c贸mo obtener el valor del evento en input. Si almacenaremos este conocimiento en el nuestro LogInForm, entonces es obvio que al usar nuestroInputFielden otra forma, tendremos que duplicar nuestro conocimiento, o llevarlo a una abstracci贸n separada y usarlo desde all铆. Y seg煤n el principio KISS, deber铆amos tener el m铆nimo n煤mero posible de abstracciones. De hecho, 驴por qu茅 necesitamos crear otra abstracci贸n si solo podemos poner esta l贸gica en la nuestra InputFieldy el c贸digo externo no sabr谩 nada sobre c贸mo funciona la entrada en el interior InputField? Simplemente tomar谩 un significado ya hecho, lo mismo que pasa hacia adentro.

Si est谩 confundido de que pueda necesitar un evento en el futuro, recuerde el principio de YAGNI. Siempre puedes agregar un accesorio adicional onChangeEventa nuestro componente InputField.

Hasta entonces se InputFieldver谩 as铆:
C贸digo de copia
const InputField = ({ label, type, value, onChange }) => (
  <label>
    {label}
    <input
      type={type}
      value={value}
      onChange={e => onChange(e.target.value)}
    />
  </label>
);



Por lo tanto, se observa uniformidad del tipo durante la entrada y salida en el componente y se oculta la verdadera naturaleza de lo que est谩 sucediendo para el c贸digo externo. Si en el futuro necesitamos otro componente ui, por ejemplo, casilla de verificaci贸n o selecci贸n, entonces tambi茅n mantendremos la uniformidad de tipo en la entrada-salida.

Este enfoque nos brinda flexibilidad adicional, ya que nuestro formulario puede funcionar con cualquier fuente de entrada-salida sin la necesidad de producir controladores 煤nicos adicionales para cada uno.

Este truco heur铆stico est谩 incorporado de forma predeterminada en muchos marcos. Por ejemplo, esta es la idea principal v-modelen la Vueque muchas personas adoran por su facilidad para trabajar con formularios.

Volvamos al negocio, actualice nuestro componente LogInFormde acuerdo con los cambios en InputField:
C贸digo de copia
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>
  );
};



Ya se ve bastante bien, 隆pero podemos hacerlo a煤n mejor!

Callbackque se pasa onChangesiempre hace lo mismo. Solo se cambia la clave: contrase帽a, correo electr贸nico, apodo. Por lo tanto, podemos sustituirla por una llamada a la funci贸n: handleChange('password').

Implementemos esta funci贸n:
C贸digo de copia
  const handleChange = fieldName => fieldValue => {
    setLogInData({
      ...logInData,
      [fieldName]: fieldValue,
    });
  };



Como puede ver, la funci贸n recibe el argumento y lo almacena en el cierre del controlador, y el controlador vuelve inmediatamente al c贸digo externo. Las funciones que parecen tomar argumentos uno a la vez para una llamada tambi茅n se denominan curry.

Veamos el c贸digo resultante:
C贸digo de copia
  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 es una implementaci贸n breve, concisa y moderadamente declarativa de la tarea. Refactorizar m谩s en el contexto de la tarea, en mi opini贸n, no tiene sentido.




Qu茅 m谩s se puede hacer?



Si tiene muchas formas en el proyecto, puede colocar el c谩lculo de handleChange en un gancho separado useFieldChange:
C贸digo de copia
  // hooks/useFieldChange.js
  const useFieldChange = setState => fieldName => fieldValue => {
    setState(state => ({
      ...state,
      [fieldName]: fieldValue,
    }));
  };
  // LogInForm.js
  const handleChange = useFieldChange(setLogInData);



Como se trata de una funci贸n pura (es decir, la primera vez que se llama porque siempre devuelve la misma funci贸n), no tiene que ser un enlace. Pero el gancho parece conceptualmente una soluci贸n m谩s correcta y natural para React.

Tambi茅n puede agregar soporte callbacken el lugar fieldValuepara replicar completamente el comportamiento habitual setStatede React:
C贸digo de copia
  const isFunc = val => typeof val === "function";

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



Un ejemplo de uso con nuestro formulario:
C贸digo de copia
  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>
    );
  };



Pero si su solicitud tiene solo un formulario, 隆no necesita hacer todo esto! Porque contradice el principio YAGNI, siguiendo el cual no debemos hacer lo que no necesitamos para resolver un problema espec铆fico. Si solo tiene una forma, los beneficios reales de tales movimientos no son suficientes. Despu茅s de todo, redujimos el c贸digo de nuestro componente solo en 3 l铆neas, pero introdujimos una abstracci贸n adicional, ocultando una cierta l贸gica de la forma, que es mejor mantener en la superficie.




隆Oh no! 隆No lo hagas, por favor!



Formas de configuraci贸n en forma


Las configuraciones bloqueadas son como una configuraci贸n de paquete web, solo para el formulario.

Mejor con un ejemplo, mira este c贸digo:
C贸digo de copia
  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>
  );



A algunos les puede parecer que el c贸digo est谩 duplicado aqu铆, porque llamamos al mismo componente InputField, pasando all铆 la misma etiqueta, valor y par谩metros onChange. Y comienzan a SECAR su propio c贸digo para evitar la duplicaci贸n imaginaria.
A menudo hacen esto as铆:
C贸digo de copia
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, con 17 l铆neas de c贸digo jsx obtenemos 16 l铆neas de la configuraci贸n. 隆Bravo! Esto es lo que entiendo DRY. Si tenemos 100 de estas entradas aqu铆, entonces obtenemos 605 y 506 l铆neas, respectivamente.

Pero, como resultado, obtuvimos un c贸digo m谩s complejo, violando el principio KISS. De hecho, ahora consta de 2 abstracciones: campos y un algoritmo (s铆, un algoritmo tambi茅n es una abstracci贸n), lo que lo convierte en un 谩rbol de elementos React. Al leer este c贸digo, tendremos que saltar constantemente entre estas abstracciones.

Pero el mayor problema con este c贸digo es su soporte y extensi贸n.
Imagina lo que le suceder谩 si:
  • Se agregar谩n algunos tipos de campos m谩s con diferentes propiedades posibles
  • los campos se representan o no se basan en la entrada de otros campos
  • algunos campos requieren un procesamiento adicional al cambiar
  • los valores en selecciones dependen de selecciones anteriores

Esta lista puede continuar durante mucho tiempo ...

Para implementar esto, siempre tendr谩 que almacenar algunas funciones en su configuraci贸n, y el c贸digo que representa la configuraci贸n se convertir谩 gradualmente en Frankenstein, tomando un mont贸n de accesorios diferentes y lanz谩ndolos sobre diferentes componentes.

El c贸digo que se escribi贸 anteriormente, basado en la composici贸n de los componentes, se expandir谩 silenciosamente, cambiar谩 a su gusto, sin ser muy complicado debido a la falta de abstracci贸n intermedia, un algoritmo que construye un 谩rbol de elementos React desde nuestra configuraci贸n. Este c贸digo no est谩 duplicado. Simplemente sigue un cierto patr贸n de dise帽o y, por lo tanto, parece "modelado".

Optimizaci贸n in煤til


Despu茅s de que aparecieron los ganchos en React, hubo una tendencia a envolver todos los manipuladores y componentes indiscriminadamente en useCallbacky memo. 隆Por favor, no hagas eso! Los desarrolladores de React no proporcionaron estos ganchos porque React es lento y todo necesita ser optimizado. Proporcionan espacio para optimizar su aplicaci贸n en caso de que tenga problemas de rendimiento. E incluso si encuentra tales problemas, no necesita envolver todo el proyecto memoy useCallback. Use Profiler para identificar problemas y solo luego memorizar en el lugar correcto.

La memorizaci贸n siempre har谩 que su c贸digo sea m谩s complicado, pero no siempre m谩s productivo.

Veamos el c贸digo de un proyecto real. Compare c贸mo se ve la funci贸n con useCallbacky sin:
C贸digo de copia
  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);
  };



La legibilidad del c贸digo aument贸 claramente despu茅s de la eliminaci贸n del contenedor, porque el c贸digo ideal es su falta.

El rendimiento de este c贸digo no ha aumentado, porque esta funci贸n se usa as铆:
C贸digo de copia
  <RightFooterButton onClick={applyFilters}>APPLY</RightFooterButton>



De d贸nde RightFooterButton: es solo styled.buttonde styled-components, que se actualizar谩 muy r谩pidamente. Pero el consumo de memoria aumenta nuestra aplicaci贸n porque React siempre se mantendr谩 en la memoria selectedMetricsStatus, onApplyFilterClicky la versi贸n de la funci贸n applyFilters, relevante para estas dependencias.

Si estos argumentos no son suficientes para usted, lea el art铆culo que trata este tema m谩s ampliamente.




recomendaciones


  • Las formas de reacci贸n son f谩ciles. Los problemas con ellos surgen debido a los propios desarrolladores y la documentaci贸n de React, en la que este tema no se divulga con suficiente detalle.
  • aka value onChange . useFieldChange, , , v-model Vue .
  • KISS YAGNI. DRY, , .
  • , , React- .
  • , .




P.S.


Inicialmente, plane茅 escribir sobre la validaci贸n declarativa de formas anidadas complejas. Pero, al final, decid铆 preparar a la audiencia para esto, de modo que la imagen fuera lo m谩s completa posible.
El siguiente tutorial tratar谩 sobre la validaci贸n del formulario simple implementado en este tutorial.
隆Muchas gracias a todos los que leyeron hasta el final!

No importa c贸mo escriba el c贸digo o lo que haga, lo principal es disfrutarlo.

All Articles