Componentes de orden superior en reacción

Recientemente, publicamos material sobre funciones de orden superior en JavaScript dirigido a aquellos que aprenden JavaScript. El artículo que estamos traduciendo hoy está destinado a desarrolladores principiantes de React. Se centra en componentes de orden superior (HOC).



Principio DRY y componentes de orden superior en React


No podrá avanzar lo suficiente en el estudio de la programación y no se encontrará con el principio casi culto de DRY (no se repita, no repita). A veces sus seguidores van demasiado lejos, pero, en la mayoría de los casos, vale la pena luchar por el cumplimiento. Aquí vamos a hablar sobre el patrón de desarrollo React más popular, que garantiza el cumplimiento del principio DRY. Se trata de componentes de orden superior. Para comprender el valor de los componentes de orden superior, primero formulemos y comprendamos el problema que están destinados a resolver.

Suponga que necesita recrear un panel de control similar al panel Stripe. Muchos proyectos tienen la propiedad de desarrollar de acuerdo con el esquema, cuando todo va bien hasta el momento en que se completa el proyecto. Cuando piensa que el trabajo está casi terminado, observa que el panel de control tiene muchas sugerencias de herramientas diferentes que deberían aparecer al pasar el mouse sobre ciertos elementos.


Panel de control e información sobre herramientas

Para implementar dicha funcionalidad, puede usar varios enfoques. Decidió hacer esto: determine si el puntero está por encima de un componente individual, y luego decida si mostrar una pista para él o no. Hay tres componentes que deben equiparse con una funcionalidad similar. Estos son Info , TrendChart y DailyChart .

Comencemos con el componente de Info . En este momento es un simple icono SVG.

 class Info extends React.Component { render() {   return (     <svg       className="Icon-svg Icon--hoverable-svg"       height={this.props.height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   ) } } 

Ahora debemos hacer que este componente pueda determinar si el puntero del mouse está por encima o no. Puede usar los eventos de mouse onMouseOver y onMouseOut para esto. Se onMouseOver a la función pasada a onMouseOver si el puntero del mouse ha caído en el área del componente, y la función pasada a onMouseOut se llamará cuando el puntero abandone el componente. Para organizar todo esto de una manera que sea aceptada en React, agregamos la propiedad hovering al componente, que se almacena en el estado, lo que nos permite volver a representar el componente mostrando u ocultando la información sobre herramientas si esta propiedad cambia.

 class Info extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id} />         : null}       <svg         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}         className="Icon-svg Icon--hoverable-svg"         height={this.props.height}         viewBox="0 0 16 16" width="16">           <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />       </svg>     </>   ) } } 

Resultó bastante bien. Ahora necesitamos agregar la misma funcionalidad a dos componentes más: TrendChart y DailyChart . El mecanismo anterior para el componente Info funciona bien, lo que no está roto no necesita ser reparado, así que vamos a recrearlo en otros componentes usando el mismo código. Recicle el código para el componente TrendChart .

 class TrendChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='trend'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Probablemente ya entendiste qué hacer a continuación. Lo mismo se puede hacer con nuestro último componente: DailyChart .

 class DailyChart extends React.Component { state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) render() {   return (     <>       {this.state.hovering === true         ? <Tooltip id={this.props.id}/>         : null}       <Chart         type='daily'         onMouseOver={this.mouseOver}         onMouseOut={this.mouseOut}       />     </>   ) } } 

Ahora todo está listo. Es posible que ya hayas escrito algo similar en React. Este, por supuesto, no es el peor código del mundo, pero no sigue el principio DRY particularmente bien. Como puede ver, al analizar el código del componente, nosotros, en cada uno de ellos, repetimos la misma lógica.

El problema que enfrentamos ahora debería ser extremadamente claro. Este es un código duplicado. Para resolverlo, queremos eliminar la necesidad de copiar el mismo código en los casos en que lo que ya hemos implementado es necesario para un nuevo componente. ¿Cómo solucionarlo? Antes de hablar sobre esto, nos centraremos en varios conceptos de programación que facilitarán enormemente la comprensión de la solución propuesta aquí. Estamos hablando de devoluciones de llamada y funciones de orden superior.

Funciones de orden superior


Las funciones en JavaScript son objetos de primera clase. Esto significa que, como objetos, matrices o cadenas, pueden asignarse a variables, pasarse a funciones como argumentos o devolverse de otras funciones.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } addFive(10, add) // 15 

Si no está acostumbrado a este comportamiento, el código anterior puede parecerle extraño. Hablemos de lo que está pasando aquí. Es decir, pasamos la función add a la función addFive como argumento, le addReference nombre a addReference y luego la llamamos.

Cuando se utilizan tales construcciones, una función que se pasa a otra como argumento se denomina devolución de llamada (función de devolución de llamada), y una función que recibe otra función como argumento se denomina función de orden superior.

Nombrar entidades en la programación es importante, así que aquí está el mismo código utilizado en el que los nombres se cambian de acuerdo con los conceptos que representan.

 function add (x,y) { return x + y } function higherOrderFunction (x, callback) { return callback(x, 5) } higherOrderFunction(10, add) 

Este patrón debería parecerte familiar. El hecho es que si usó, por ejemplo, métodos de matriz de JavaScript, trabajó con jQuery o lodash, entonces ya usó funciones de orden superior y devoluciones de llamada.

 [1,2,3].map((i) => i + 5) _.filter([1,2,3,4], (n) => n % 2 === 0 ); $('#btn').on('click', () => console.log('Callbacks are everywhere') ) 

Volvamos a nuestro ejemplo. ¿Qué pasa si, en lugar de simplemente crear la función addFive , queremos crear la función addTwenty , addTwenty y otras similares? Dada la addFive que se addFive función addFive , tendremos que copiar su código y cambiarlo para crear las funciones mencionadas anteriormente basadas en él.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } function addTen (x, addReference) { return addReference(x, 10) } function addTwenty (x, addReference) { return addReference(x, 20) } addFive(10, add) // 15 addTen(10, add) // 20 addTwenty(10, add) // 30 

Cabe señalar que nuestro código no era tan pesadilla, pero está claro que muchos fragmentos se repiten. Nuestro objetivo es que podamos crear tantas funciones que agreguen ciertos números a los números que se les addFive ( addFive , addTen , addTwenty , etc.) todo lo que necesitemos, mientras minimizamos la duplicación de código. ¿Quizás para lograr esto necesitamos crear una función makeAdder ? Esta función puede tomar un cierto número y un enlace a la función de add . Dado que el propósito de esta función es crear una nueva función que agregue el número que se le pasó al dado, podemos hacer que la función makeAdder devuelva una nueva función que contiene un cierto número (como el número 5 en makeFive ) y que podría tomar números para agregar a ese número.

Eche un vistazo a un ejemplo de la implementación de los mecanismos anteriores.

 function add (x, y) { return x + y } function makeAdder (x, addReference) { return function (y) {   return addReference(x, y) } } const addFive = makeAdder(5, add) const addTen = makeAdder(10, add) const addTwenty = makeAdder(20, add) addFive(10) // 15 addTen(10) // 20 addTwenty(10) // 30 

Ahora podemos crear tantas funciones de add como sea necesario, mientras minimizamos la cantidad de duplicación de código.

Si es interesante, el concepto de que hay una determinada función que procesa otras funciones para que puedan usarse con menos parámetros que antes se llama "aplicación parcial de la función". Este enfoque se utiliza en la programación funcional. Un ejemplo de su uso es el método .bind utilizado en JavaScript.

Todo esto es bueno, pero ¿qué tiene que ver React con el problema anterior de duplicar el código para procesar eventos del mouse al crear nuevos componentes que necesitan esta característica? El hecho es que al igual que la función de orden superior makeAdder nos ayuda a minimizar la duplicación de código, lo que se llama el "componente de orden superior" nos ayudará a lidiar con el mismo problema en una aplicación React. Sin embargo, aquí todo se verá un poco diferente. Es decir, en lugar de un esquema de trabajo, durante el cual una función de orden superior devuelve una nueva función que llama a una devolución de llamada, un componente de orden superior puede implementar su propio esquema. Es decir, puede devolver un nuevo componente que representa un componente que desempeña el papel de una "devolución de llamada". Quizás ya hayamos dicho muchas cosas, así que es hora de pasar a los ejemplos.

Nuestra función de orden superior


Esta característica tiene las siguientes características:

  • Ella es una función.
  • Ella acepta, como argumento, una devolución de llamada.
  • Devuelve una nueva función.
  • La función que devuelve puede llamar a la devolución de llamada original que se pasó a nuestra función de orden superior.

 function higherOrderFunction (callback) { return function () {   return callback() } } 

Nuestro componente de mayor orden


Este componente se puede caracterizar de la siguiente manera:

  • Es un componente.
  • Como argumento, toma otro componente.
  • Devuelve un nuevo componente.
  • El componente que devuelve puede representar el componente original pasado al componente de orden superior.

 function higherOrderComponent (Component) { return class extends React.Component {   render() {     return <Component />   } } } 

Implementación HOC


Ahora que, en términos generales, hemos descubierto exactamente qué acciones realiza el componente de orden superior, comenzaremos a realizar cambios en nuestro código React. Si recuerdas, la esencia del problema que estamos resolviendo es que el código que implementa la lógica de procesamiento de eventos del mouse debe copiarse a todos los componentes que necesitan esta función.

 state = { hovering: false } mouseOver = () => this.setState({ hovering: true }) mouseOut = () => this.setState({ hovering: false }) 

Dado esto, necesitamos nuestro componente de orden superior (llamémoslo con withHover ) para encapsular el código de procesamiento de eventos del mouse y luego pasar la propiedad hovering a los componentes que representa. Esto nos permitirá evitar la duplicación del código correspondiente al colocarlo en el componente withHover .

En definitiva, esto es lo que queremos lograr. Siempre que necesitemos un componente que necesite tener una idea de su propiedad hovering , podemos pasar este componente a un componente de orden superior withHover . Es decir, queremos trabajar con componentes como se muestra a continuación.

 const InfoWithHover = withHover(Info) const TrendChartWithHover = withHover(TrendChart) const DailyChartWithHover = withHover(DailyChart) 

Luego, cuando se representa lo que withHover , será el componente fuente al que se pasa la propiedad hovering .

 function Info ({ hovering, height }) { return (   <>     {hovering === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } 

De hecho, ahora solo tenemos que implementar el componente withHover . De lo anterior, se puede entender que debe realizar tres acciones:

  • Lleve un argumento a Componente.
  • Devuelve un nuevo componente.
  • Renderice el argumento Componente pasándole la propiedad hovering .

▍ Aceptar el argumento Componente


 function withHover (Component) { } 

▍ Devolver un nuevo componente


 function withHover (Component) { return class WithHover extends React.Component { } } 

▍ Representación del componente Componente con la propiedad flotante pasada


Ahora nos enfrentamos a la siguiente pregunta: ¿cómo llegar a la propiedad hovering ? De hecho, ya escribimos el código para trabajar con esta propiedad. Solo tenemos que agregarlo al nuevo componente y luego pasarle la propiedad de hovering al representar el componente pasado al componente de orden superior en forma del argumento Component .

 function withHover(Component) { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component hovering={this.state.hovering} />       </div>     );   } } } 

Prefiero hablar sobre estas cosas de la siguiente manera (como dice la documentación de React): un componente convierte las propiedades en una interfaz de usuario, y un componente de orden superior convierte un componente en otro componente. En nuestro caso, transformaremos los componentes Info , TrendChart y DailyChart en nuevos componentes que, gracias a la propiedad de DailyChart , sepan si el puntero del mouse está sobre ellos.

Notas adicionales


En este punto, hemos revisado toda la información básica sobre los componentes de orden superior. Sin embargo, hay algunas cosas más importantes para discutir.

Si echa un vistazo a nuestro HOC con withHover , notará que tiene al menos un punto débil. Implica que el componente receptor de la propiedad hovering no experimentará ningún problema con esta propiedad. En la mayoría de los casos, es probable que esta suposición esté justificada, pero puede suceder que esto sea inaceptable. Por ejemplo, ¿qué pasa si un componente ya tiene una propiedad hovering ? En este caso, habrá una colisión de nombres. Por lo tanto, se puede realizar un withHover en el componente withHover , que es permitir que el usuario de este componente especifique qué nombre debe tener la propiedad hovering pasa a los componentes. Como withHover es solo una función, reescribámosla para que withHover segundo argumento que establezca el nombre de la propiedad que se pasará al componente.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Ahora, gracias al mecanismo de parámetro predeterminado ES6, establecemos el valor estándar del segundo argumento como hovering , pero si el usuario del componente withHover quiere cambiar esto, puede pasar, en este segundo argumento, el nombre que necesita.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } function Info ({ showTooltip, height }) { return (   <>     {showTooltip === true       ? <Tooltip id={this.props.id} />       : null}     <svg       className="Icon-svg Icon--hoverable-svg"       height={height}       viewBox="0 0 16 16" width="16">         <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />     </svg>   </> ) } const InfoWithHover = withHover(Info, 'showTooltip') 

Problema con la implementación de Over


Es posible que haya notado otro problema con la implementación de withHover . Si analizamos nuestro componente Info , notará que, entre otras cosas, acepta la propiedad de height . La forma en que tenemos todo organizado ahora significa que la height se establecerá en undefined . La razón de esto es porque el componente withHover es el componente responsable de representar lo que se le pasa como argumento Component . Ahora no estamos transfiriendo ninguna propiedad que no sea el hovering que creamos al Component Componente.

 const InfoWithHover = withHover(Info) ... return <InfoWithHover height="16px" /> 

La propiedad de height se pasa al componente InfoWithHover . ¿Y cuál es este componente? Este es el componente del que withHover con withHover .

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     console.log(this.props) // { height: "16px" }     const props = {       [propName]: this.state.hovering     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Dentro del componente WithHover this.props.height es 16px , pero en el futuro no haremos nada con esta propiedad. Necesitamos hacer que esta propiedad pase al argumento Component , que estamos representando.

 render() {     const props = {       [propName]: this.state.hovering,       ...this.props,     }     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     ); } 

Sobre los problemas de trabajar con componentes de terceros de primer orden


Creemos que ya ha apreciado las ventajas de usar componentes de orden superior para reutilizar la lógica en varios componentes sin la necesidad de copiar el mismo código. Ahora preguntémonos si hay fallas en los componentes de orden superior. Esta pregunta puede ser respondida positivamente, y ya nos hemos encontrado con estas deficiencias.

Cuando se usa HOC, se produce una inversión de control . Imagine que estamos utilizando un componente de orden superior que no fue desarrollado por nosotros, como el HOC withRouter Router React Router. Según la documentación, withRouter pasará las propiedades de match , location e history al componente que envolvió al representarlo.

 class Game extends React.Component { render() {   const { match, location, history } = this.props // From React Router   ... } } export default withRouter(Game) 

Tenga en cuenta que no estamos creando un elemento Game (es decir, <Game /> ). Transferimos completamente nuestro componente React Router y confiamos en este componente no solo para renderizar, sino también para pasar las propiedades correctas a nuestro componente. Ya hemos encontrado este problema antes cuando hablamos de un posible conflicto de nombre al pasar la propiedad hovering . Para solucionar esto, decidimos permitir que el withHover HOC withHover use el segundo argumento para configurar el nombre de la propiedad correspondiente. Al usar el HOC de otra persona con withRouter , no tenemos esa oportunidad. Si las propiedades de match , location o history ya se usan en el componente Game , entonces podemos decir que no tuvimos suerte. Es decir, tenemos que cambiar estos nombres en nuestro componente o rechazar el uso de HOC con withRouter .

Resumen


Hablando sobre HOC en React, hay dos cosas importantes a tener en cuenta. En primer lugar, HOC es solo un patrón. Los componentes de orden superior ni siquiera pueden llamarse algo específico de React, a pesar del hecho de que están relacionados con la arquitectura de la aplicación. En segundo lugar, para desarrollar aplicaciones React, no necesita saber acerca de los componentes de orden superior. Puede que no estés familiarizado con ellos, pero escribe excelentes programas. Sin embargo, como en cualquier negocio, cuantas más herramientas tenga, mejor será el resultado de su trabajo. Y, si escribe aplicaciones usando React, se perjudicará sin agregar HOC a su arsenal.

Estimados lectores! ¿Utiliza componentes de orden superior en React?

Source: https://habr.com/ru/post/es428572/


All Articles