¿Por qué es un anti-patrón?

Hola. En septiembre, varios cursos sobre desarrollo de JS comenzarán a la vez en OTUS, a saber: Desarrollador de JavaScript. Desarrollador profesional de JavaScript. Desarrollador Basic y React.js. Anticipándonos al inicio de estos cursos, hemos preparado otra traducción interesante para usted, y también ofrecemos registrarse para recibir lecciones de demostración gratuitas sobre los siguientes temas:





Ahora pasemos al artículo.










Cuando comencé a aprender React, había algunas cosas que no entendía. Y creo que casi todos los que están familiarizados con React se hacen las mismas preguntas. Estoy seguro de esto porque la gente está construyendo bibliotecas enteras para resolver problemas urgentes. Aquí hay dos preguntas principales que parecen preocupar a casi todos los desarrolladores de React:



¿Cómo accede un componente a la información (especialmente una variable de estado) que se encuentra en otro componente? ¿Cómo llama un componente a una función que está en otro componente?



Los desarrolladores de JavaScript en general (y los desarrolladores de React en particular) se han inclinado cada vez más hacia la escritura de las llamadas funciones puras últimamente. Funciones que no están asociadas con cambios de estado. Funciones que no necesitan conexiones a bases de datos externas. Funciones que son independientes de lo que ocurre fuera de ellas.



Por supuesto, las funciones puras son un objetivo noble. Pero si está desarrollando una aplicación más o menos compleja, no podrá limpiar todas las funciones. Sin duda, llegará un momento en el que tendrá que crear al menos algunos componentes que de alguna manera estén relacionados con otros componentes. Intentar evitarlo es ridículo. Estas conexiones entre componentes se denominan dependencias .



En general, las dependencias son malas y es mejor usarlas solo cuando sea necesario. Pero, de nuevo, si su aplicación ha crecido, algunos de sus componentes dependerán necesariamente unos de otros. Por supuesto, los desarrolladores de React lo saben, por lo que descubrieron cómo hacer que un componente pase información crítica, o funciones, a sus componentes secundarios.



Enfoque estándar: use accesorios para pasar valores



Cualquier valor de estado se puede pasar a otro componente a través de accesorios. Cualquier función se puede pasar a los componentes secundarios a través de los mismos accesorios. Así es como los descendientes saben qué valores de estado están almacenados en el árbol y pueden potencialmente invocar acciones en los componentes principales. Todo esto, por supuesto, es bueno. Pero los desarrolladores de React están preocupados por un problema en particular.



La mayoría de las aplicaciones tienen capas. En aplicaciones complejas, las estructuras se pueden anidar muy profundamente. La arquitectura general podría verse así:



App→ se refiere a → ContentArea

ContentArea→ se refiere a → MainContentArea

MainContentArea→ se refiere a → MyDashboard

MyDashboard→ se refiere a → MyOpenTickets

MyOpenTickets→ se refiere a → TicketTable

TicketTable→ se refiere a una secuencia → TicketRow

Todos TicketRow→ se refiere a →TicketDetail



En teoría, esta guirnalda se puede enrollar durante mucho tiempo. Todos los componentes son parte del todo. Más precisamente, parte de una jerarquía. Pero aquí surge la pregunta:



¿Puede el componente TicketDetaildel ejemplo anterior leer los valores de estado que están almacenados ContentArea? O. ¿Puede un componente TicketDetailllamar a funciones que están en ContentArea?

La respuesta a ambas preguntas es sí. En teoría, todos los descendientes pueden conocer todas las variables almacenadas en los componentes principales. También pueden llamar a funciones de ancestros, pero con una gran advertencia. Esto es posible solo si dichos valores (valores de estado o función) se pasan explícitamente a los descendientes a través de accesorios. De lo contrario, los valores de función o estado del componente no estarán disponibles para su componente hijo.



En pequeñas aplicaciones y utilidades, esto no juega un papel especial. Por ejemplo, si un componente TicketDetailnecesita acceder a las variables de estado que están almacenadas TicketRow, basta con hacer que el componente TicketRow→ pase estos valores a su descendiente → a TicketDetailtravés de uno o más accesorios. Lo mismo ocurre cuando un componente TicketDetailnecesita llamar a una función que está en TicketRow. Componente TicketRow→ pasará esta función a su descendiente → a TicketDetailtravés de prop. El dolor de cabeza comienza cuando un componente muy abajo en el árbol necesita acceder al estado o función del componente en la parte superior de la jerarquía.



Para resolver este problema, React ha pasado tradicionalmente variables y funciones a todos los niveles. Pero esto satura el código, consume recursos y requiere una planificación seria. Tendríamos que pasar valores a muchos niveles como este:



ContentAreaMainContentAreaMyDashboardMyOpenTicketsTicketTableTicketRowTicketDetail



Es decir, para pasar una variable de estado de ContentAreaa TicketDetail, necesitamos hacer mucho trabajo. Los desarrolladores experimentados entienden que existe una fea cadena larga de valores y funciones que pasan como accesorios a través de niveles intermedios de componentes. La solución es tan engorrosa que incluso dejé de aprender React un par de veces por eso.



El monstruo llamado Redux



No soy el único que piensa que pasar todos los valores de estado y todas las funciones comunes a los componentes a través de accesorios es muy poco práctico. Es poco probable que encuentre una aplicación React compleja que no venga con una herramienta de administración de estado. No hay tan pocas herramientas de este tipo. Personalmente, amo MobX. Desafortunadamente, Redux se considera el "estándar de la industria".



Redux es una creación de los creadores del núcleo React. Es decir, primero crearon una maravillosa biblioteca de React. Pero inmediatamente se dieron cuenta de que era casi imposible administrar el estado con sus medios. Si no hubieran encontrado una manera de resolver los problemas inherentes de esta biblioteca (por lo demás excelente), muchos de nosotros nunca hubiéramos oído hablar de React.



Entonces se les ocurrió Redux.

Si React es Mona Lisa, Redux es un bigote que se le atribuye. Si está utilizando Redux, tendrá que escribir una tonelada de código repetitivo en casi todos los archivos de proyecto. La resolución de problemas y la lectura de códigos se vuelven un infierno. La lógica empresarial se lleva al patio trasero. El código contiene confusión y vacilación.



Pero si los desarrolladores tienen una opción: React + Redux o React sin herramientas de administración de estado de terceros , casi siempre eligen React + Redux. Dado que la biblioteca Redux fue desarrollada por los autores principales de React, se considera una solución aprobada por defecto. Y la mayoría de los desarrolladores prefieren utilizar soluciones que hayan sido aprobadas tácitamente como esta.



Por supuesto, Redux creará toda una red de dependencias.en su aplicación React. Pero, para ser justos, cualquier herramienta de gestión estatal genérica hará lo mismo. La herramienta de gestión del estado es un repositorio compartido de variables y funciones. Tales funciones y variables pueden ser utilizadas por cualquier componente que tenga acceso al almacenamiento compartido. Esto tiene un inconveniente obvio: todos los componentes se vuelven dependientes del almacenamiento compartido.



La mayoría de los desarrolladores de React que conozco que han intentado resistirse a usar Redux finalmente se rindieron. (Porque ... la resistencia es inútil). Conozco a mucha gente que inmediatamente odia a Redux. Pero cuando se enfrentaban a una elección - Redux o "vamos a encontrar otra Reaccionar desarrollador" - tiraron a sí mismoshan aceptado abrazar a Redux como parte integral de sus vidas. Es como impuestos. Como un examen rectal. Como ir al dentista.



Reaccionar valores compartidos en React



Soy demasiado terco para rendirme tan fácilmente. Después de mirar Redux, me di cuenta de que necesitaba buscar otras soluciones. Yo puedo usar Redux. Y trabajé en equipos que usaban esta biblioteca. En general, entiendo lo que hace. Pero eso no significa que me guste Redux.

Como dije, mientras que una herramienta de administración de estado separada es indispensable, MobX es aproximadamente ... ¡un millón de veces mejor que Redux! Pero me atormenta una pregunta más seria. Toca la mente colectiva de los desarrolladores de React:



¿Por qué siempre nos aferramos primero a una herramienta de gestión de estado?



Cuando comencé a desarrollar con React, pasé muchas noches buscando soluciones alternativas. Y encontré una forma que muchos desarrolladores de React descuidan, pero ninguno de ellos puede decir por qué . Explicará.



Imagínese que en la aplicación hipotética sobre la que escribí anteriormente, creamos un archivo como este:



// components.js
let components = {};
export default components;


Y eso es todo. Solo dos líneas cortas de código. Creamos un objeto vacío, un buen objeto JS antiguo . Lo exportamos por defecto con export default.



Ahora veamos cómo se vería el código dentro del componente <ContentArea>:



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      components.ContentArea = this;
   }

   consoleLog(value) {
      console.log(value);
   }

   render() {
      return <MainContentArea/>;
   }
}


En su mayor parte, parece un componente React basado en clases perfectamente normal. Tenemos una función simple render()que accede al siguiente componente del árbol. Tenemos una pequeña función console.log()que imprime el resultado de la ejecución del código en la consola y un constructor. Pero ... hay algunos matices en el constructor .



Al principio, importamos un objeto simple components. Luego, en el constructor, agregamos una nueva propiedad al objeto componentscon el nombre del componente React actual ( this). En esta propiedad nos referimos al componente this. Ahora, cada vez que accedamos al objeto de componentes, tendremos acceso directo al componente <ContentArea>.



Veamos qué sucede en la parte inferior de la jerarquía. El componente <TicketDetail>puede ser así:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      components.ContentArea.consoleLog('it works');
      return <div>Here are the ticket details.</div>;
   }
}


Esto es lo que sucede. Cada vez que TicketDetailse renderiza consoleLog()el componente , se llamará a una función almacenada en el componente ContentArea.



Tenga en cuenta que la función consoleLog()no se transmite a través de toda la jerarquía a través de accesorios. De hecho, la función consoleLog()no se pasa a ninguna parte, ni a ninguna parte, a ningún componente.



Y, sin embargo, TicketDetailpuede llamar a la función consoleLog()que está almacenada ContentArea, porque hicimos dos cosas:



  1. ContentAreaCuando se carga, el componente agrega un enlace a sí mismo al objeto compartido de componentes.
  2. TicketDetailUna vez cargado, el componente importaba un objeto compartido components, es decir, tenía acceso directo al componente ContentArea, a pesar de que las propiedades ContentAreano se pasaban al componente a TicketDetailtravés de props.


Este enfoque no solo funciona con funciones / devoluciones de llamada. Se puede utilizar para consultar directamente los valores de las variables de estado. Imaginemos lo que se <ContentArea>ve así:



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   render() {
      return <MainContentArea/>;
   }
}


Entonces podemos escribir <TicketDetail>así:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return <div>Here are the ticket details.</div>;
   }
}


Ahora, cada vez que se renderiza el componente <TicketDetail, buscará el valor de la variable state.reduxSucksen <ContentArea>. Si la variable devuelve un valor true, la función console.log()imprimirá un mensaje en la consola. Esto sucederá incluso si el valor de la variable ContentArea.state.reduxSucksnunca se ha pasado por el árbol, a ninguno de los componentes, a través de accesorios. Entonces, con un objeto JS subyacente simple que vive fuera del ciclo de vida estándar de React, podemos hacer que cualquier descendiente pueda leer variables de estado directamente desde cualquier padre cargado en el objeto de componentes. Incluso podemos llamar a funciones del componente padre en su descendiente.



La capacidad de llamar a una función directamente en los componentes secundarios significa que podemos cambiar el estado de los componentes principales directamente de sus elementos secundarios. Por ejemplo así.



Primero, en el componente, <ContentArea>crearemos una función simple que cambia el valor de una variable reduxSucks.



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
   }

   render() {
      return <MainContentArea/>;
   }
}


Luego, en el componente, <TicketDetail>llamaremos a este método a través del objeto components:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}


Ahora, después de cada renderizado de un componente, el <TicketDetail>usuario podrá presionar un botón que cambiará (alternará) el valor de la variable ContentArea.state.reduxSucksen tiempo real, incluso si la función ContentArea.toggleReduxSucks()nunca se ha pasado del árbol a través de accesorios.



Con este enfoque, el componente padre puede llamar a la función directamente desde su hijo. He aquí cómo hacerlo. El componente actualizado <ContentArea>se verá así:



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
   }

   render() {
      return <MainContentArea/>;
   }
}


Ahora agreguemos lógica al componente <TicketTable>. Me gusta esto:



// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';

export default class TicketTable extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucksHasBeenToggledXTimes: 0 };
      components.TicketTable = this;
   }

   incrementReduxSucksHasBeenToggledXTimes() {
      this.setState((previousState, props) => {
         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
      });      
   }

   render() {
      const {reduxSucksHasBeenToggledXTimes} = this.state;
      return (
         <>
            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
            <TicketRow data={dataForTicket1}/>
            <TicketRow data={dataForTicket2}/>
            <TicketRow data={dataForTicket3}/>
         </>
      );
   }
}


Como resultado, el componente <TicketDetail>no ha cambiado. Todavía se ve así:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}


¿Ha notado la rareza asociada con estas tres clases? En la jerarquía de nuestra aplicación ContentArea, este es el componente principal de TicketTable, que es el componente principal de TicketDetail. Esto significa que cuando montamos un componente ContentArea, este todavía no "sabe" acerca de su existencia TicketTabley la función toggleReduxSucks()escrita en ContentAreaimplícitamente llama a la función hija:

incrementReduxSucksHasBeenToggledXTimes()Resulta que el código no funcionará , ¿verdad?



Pero no.



Mira. Hemos creado varios niveles en la aplicación y solo hay una forma de llamar a la función toggleReduxSucks(). Me gusta esto.



  1. Montamos y renderizamos ContentArea.
  2. Durante este proceso, se carga una referencia a los componentes en el objeto de componentes ContentArea.
  3. El resultado se monta y se renderiza TicketTable.
  4. Durante este proceso, se carga una referencia a los componentes en el objeto de componentes TicketTable.
  5. El resultado se monta y se renderiza TicketDetail.
  6. « reduxSucks» (Toggle reduxSucks).
  7. « reduxSucks».
  8. toggleReduxSucks(), ContentArea.
  9. incrementReduxSucksHasBeenToggledXTimes() TicketTable .
  10. , , « reduxSucks», TicketTable components. toggleReduxSucks() ContentArea incrementReduxSucksHasBeenToggledXTimes(), TicketTable, components.


Resulta que la jerarquía de nuestra aplicación nos permite agregar un ContentAreaalgoritmo al componente que llamará a una función desde el componente hijo, a pesar de que el componente ContentAreano conocía la existencia del componente cuando se montóTicketTable .



Herramientas de gestión patrimonial: volcado



Como expliqué, estoy profundamente convencido de que Redux no es rival para MobX. Y cuando tengo el privilegio de trabajar en un proyecto desde cero (desafortunadamente no a menudo), siempre hago campaña para MobX. No para Redux. Pero cuando desarrollo mis propias aplicaciones , rara vez utilizo herramientas de administración de estado de terceros, casi nunca . En cambio, solo guardo en caché objetos / componentes siempre que sea posible. Y si este enfoque no funciona, a menudo recurro a la solución predeterminada en React, es decir, simplemente paso funciones / variables de estado a través de accesorios.



"Problemas" conocidos con este enfoque



Soy muy consciente de que mi idea de almacenar en caché el objeto subyacente componentsno siempre es adecuada para resolver problemas de estado / función compartidos. A veces, este enfoque puede ... jugar una broma cruel . O puede que no funcione en absoluto . Aquí hay algo para tener en cuenta.



  • Funciona mejor con solteros .



    Por ejemplo, en nuestra jerarquía, el componente <TicketTable> contiene componentes <TicketRow> con una relación de cero a muchos. Si desea almacenar en caché la referencia a cada componente potencial dentro de los componentes <TicketRow> (y sus componentes secundarios <TicketDetail>) en la caché de componentes, debe almacenarlos en una matriz, y esto puede ser complicado. Siempre he evitado esto.
  • components , / , components. .

    , . , , . / , , components.
  • , components, ( setState()), setState(), .




Ahora que he explicado mi enfoque y algunas de sus limitaciones, debo advertirles. Desde que descubrí este enfoque, lo he estado compartiendo con personas que se consideran desarrolladores profesionales de React. Cada vez responden lo mismo:



Hmm ... No hagas eso. Fruncen el ceño y actúan como si acabara de estropear el aire. Algo en mi enfoque les parece ... incorrecto . Al mismo tiempo, nadie me ha explicado todavía, basándose en su rica experiencia práctica, qué es exactamente lo que está mal. Es solo que todos consideran mi enfoque ... una blasfemia .



Por lo tanto, incluso si le gusta este enfoque o si lo encuentra conveniente en algunas situaciones, no recomiendohable de ello en una entrevista si desea obtener un trabajo como desarrollador de React. Creo que incluso hablando con otros desarrolladores de React, debes pensar un millón de veces antes de hablar sobre este método, o tal vez es mejor no decir nada en absoluto.



Descubrí que los desarrolladores de JS, y los desarrolladores de React en particular, pueden ser demasiado categóricos . A veces explican por qué el Método A es "incorrecto" y el Método B es "correcto". Pero en la mayoría de los casos, solo miran un fragmento de código y lo declaran "malo", incluso si ellos mismos no pueden explicar por qué.



Entonces, ¿por qué este enfoque es tan molesto para los desarrolladores de React?



Como dije, ninguno de mis colegas pudo responder razonablemente por qué mi método es malo. Y si alguien está dispuesto a honrarme con una respuesta, suele ser una de las siguientes excusas (hay pocas).



  • , .



    .... , , Redux ( MobX, ) / React-. , . — . , /, . : , components. , / components, / , components. /, components , components . , , . , , Redux, MobX, - .
  • React « ». … .



    … . ? , . — - « » « », , , . React, . , . . « », . , React 100 %, ( ) , .


, ?



Escribí esta publicación porque he estado usando este enfoque durante años (en proyectos personales). Y funciona muy bien . Pero cada vez que salgo de mi burbuja personal y trato de tener una conversación inteligente sobre este enfoque con otros desarrolladores de React de terceros , solo me encuentro con declaraciones categóricas y juicios tontos sobre los "estándares de la industria". ¿



Este enfoque es realmente malo ? Bien realmente. Quiero saber. Si esto es realmente un "anti-patrón", estaré inmensamente agradecido con aquellos que justifiquen su incorrección. La respuesta "no estoy acostumbrado a esto" no me conviene. No, no estoy obsesionado con este método. No estoy sugiriendo que esta sea una panacea para los desarrolladores de React. Y admito que no funciona en todas las situaciones. Pero tal vez¿Alguien puede explicarme qué tiene de malo?



Realmente quiero saber tu opinión sobre este asunto, incluso si me haces añicos.



Clases gratuitas:






All Articles