¿En qué se diferencian los componentes funcionales de React de los componentes basados ​​en clases?

¿En qué se diferencian los componentes funcionales de React de los componentes basados ​​en clases? Desde hace bastante tiempo, la respuesta tradicional a esta pregunta es: "El uso de clases le permite utilizar una gran cantidad de características de componentes, por ejemplo, el estado". Ahora, con la llegada de los ganchos , esta respuesta ya no refleja el verdadero estado de cosas.

Es posible que haya escuchado que uno de estos tipos de componentes tiene un mejor rendimiento que el otro. Pero cual? La mayoría de los puntos de referencia que prueban esto tienen fallas , por lo que sacaría conclusiones basadas en sus resultados con gran precaución. El rendimiento depende principalmente de lo que sucede en el código, y no de si los componentes funcionales o los componentes basados ​​en clases se eligen para implementar ciertas capacidades. Nuestro estudio mostró que la diferencia en el rendimiento entre los diferentes tipos de componentes es insignificante. Sin embargo, debe tenerse en cuenta que las estrategias de optimización utilizadas para trabajar con ellos difieren ligeramente.



En cualquier caso, no recomiendo reescribir los componentes existentes usando nuevas tecnologías si no hay buenas razones para ello, y si no le importa estar entre aquellos que comenzaron a usar estas tecnologías antes que los demás. Los ganchos siguen siendo una tecnología nueva (igual que la biblioteca React en 2014), y algunas "mejores prácticas" para su aplicación aún no se han incluido en los manuales de React.

¿A dónde llegamos finalmente? ¿Existen diferencias fundamentales entre los componentes funcionales de React y los componentes basados ​​en clases? Por supuesto, hay tales diferencias. Estas son diferencias en el modelo mental de usar tales componentes. En este artículo consideraré su diferencia más seria. Ha existido desde que, en 2015, aparecieron los componentes funcionales, pero a menudo se pasa por alto. Consiste en el hecho de que los componentes funcionales capturan valores representados. Hablemos de lo que eso realmente significa.

Cabe señalar que este material no constituye un intento de evaluar componentes de diferentes tipos. Solo describo la diferencia entre los dos modelos de programación en React. Si desea saber más sobre el uso de componentes funcionales a la luz de las innovaciones, consulte esta lista de preguntas y respuestas sobre ganchos.

¿Cuáles son las características del código de componentes basado en funciones y clases?


Considere este componente:

function ProfilePage(props) {  const showMessage = () => {    alert('Followed ' + props.user);  };  const handleClick = () => {    setTimeout(showMessage, 3000);  };  return (    <button onClick={handleClick}>Follow</button>  ); } 

Muestra un botón que, al presionar la función setTimeout , simula una solicitud de red y luego muestra un cuadro de mensaje que confirma la operación. Por ejemplo, si ' props.user 'Dan' está almacenado en props.user , en la ventana del mensaje, después de tres segundos, se mostrará 'Followed Dan' .

Tenga en cuenta que no importa si las funciones de flecha o las declaraciones de función se usan aquí. Una construcción de la function handleClick() formulario function handleClick() funcionará exactamente de la misma manera.

¿Cómo reescribir este componente como una clase? Si solo rehace el código que acaba de examinar y lo convierte al código de un componente basado en una clase, obtendrá lo siguiente:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; handleClick = () => {   setTimeout(this.showMessage, 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

En general, se acepta que dos de esos fragmentos de código son equivalentes. Y los desarrolladores a menudo son completamente libres, en el curso de la refactorización de código, se transforman uno en otro, sin pensar en las posibles consecuencias.


Estas piezas de código parecen ser equivalentes

Sin embargo, existe una ligera diferencia entre estos fragmentos de código. Míralos más de cerca. ¿Ves la diferencia? Por ejemplo, no la vi de inmediato.

Además, consideraremos esta diferencia, por lo tanto, para aquellos que quieran comprender la esencia de lo que está sucediendo ellos mismos, un ejemplo práctico de este código.

Antes de continuar, me gustaría enfatizar que la diferencia en cuestión no tiene nada que ver con los ganchos React. En los ejemplos anteriores, por cierto, los ganchos ni siquiera se usan. Se trata de la diferencia entre funciones y clases en React. Y si planea usar muchos componentes funcionales en sus aplicaciones React, entonces es posible que desee comprender esta diferencia.

De hecho, ilustraremos la diferencia entre funciones y clases con el ejemplo de un error que a menudo se encuentra en las aplicaciones React.

El error que es común en las aplicaciones React.


Abra la página de ejemplo que muestra una lista que le permite seleccionar perfiles de usuario y dos botones Follow que se muestran mediante los ProfilePageClass ProfilePageFunction y ProfilePageClass , funcionales y basados ​​en la clase, cuyo código se muestra arriba.

Intente, para cada uno de estos botones, realizar la siguiente secuencia de acciones:

  1. Haga clic en el botón
  2. Cambie el perfil seleccionado antes de que transcurran 3 segundos después de hacer clic en el botón.
  3. Lea el texto que se muestra en el cuadro de mensaje.

Una vez hecho esto, notará las siguientes características:

  • Cuando hace clic en el botón formado por el componente funcional con el perfil de Dan seleccionado y luego cambia al perfil de Sophie , se mostrará 'Followed Dan' en el cuadro de mensaje.
  • Si hace lo mismo con un botón formado por un componente basado en una clase, se mostrará 'Followed Sophie' .


Características de componentes basados ​​en clases

En este ejemplo, el comportamiento del componente funcional es correcto. Si me suscribí al perfil de alguien y luego cambié a otro perfil, mi componente no debería dudar de a qué perfil me suscribí. Obviamente, la implementación del mecanismo en cuestión basado en el uso de clases contiene un error (por cierto, definitivamente debes suscribirte a Sofia ).

Causas del mal funcionamiento de componentes basados ​​en clases


¿Por qué un componente basado en clases se comporta de esta manera? Para entender esto, echemos un vistazo al método showMessage en nuestra clase:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; 

Este método lee datos de this.props.user . Las propiedades en React son inmutables, por lo que no cambian. Sin embargo, this , como siempre, es una entidad mutable.

De hecho, el propósito de tener this en una clase radica en la capacidad de this para cambiar. La propia biblioteca React realiza periódicamente this mutaciones, lo que permite trabajar con las últimas versiones del método de render y los métodos del ciclo de vida de los componentes.

Como resultado, si nuestro componente se vuelve a representar durante la ejecución de la solicitud, this.props cambiará. Después de eso, el método showMessage leerá el valor del user de la entidad de props "demasiado nueva".

Esto le permite hacer una observación interesante con respecto a las interfaces de usuario. Si decimos que la interfaz de usuario, conceptualmente, es una función del estado actual de la aplicación, entonces los controladores de eventos son parte de los resultados de representación, al igual que los resultados de representación visibles. Nuestros controladores de eventos "pertenecen" a una operación de representación específica junto con propiedades y estado específicos.

Sin embargo, la programación de un tiempo de espera cuya devolución de llamada de this.props lee viola esta conexión. La devolución de showMessage showMessage no showMessage "vinculada" a ninguna operación de representación en particular, como resultado, "pierde" las propiedades correctas. Leer datos de this rompe esta conexión.

¿Cómo, por medio de componentes basados ​​en clases, resolver el problema?


Imagine que no hay componentes funcionales en React. ¿Cómo entonces resolver este problema?

Necesitamos algún mecanismo para "restaurar" la conexión entre el método de render con las propiedades correctas y la devolución de showMessage showMessage, que lee los datos de las propiedades. Este mecanismo debe ubicarse en algún lugar donde se pierda la esencia de los props con los datos correctos.

Una forma de hacerlo es leer this.props de antemano en el controlador de eventos y luego pasar explícitamente lo que se leyó a la función de devolución de llamada utilizada en setTimeout :

 class ProfilePage extends React.Component { showMessage = (user) => {   alert('Followed ' + user); }; handleClick = () => {   const {user} = this.props;   setTimeout(() => this.showMessage(user), 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Este enfoque está funcionando . Pero las construcciones adicionales utilizadas aquí, con el tiempo, conducirán a un aumento en el volumen del código y al hecho de que aumentará la probabilidad de errores en él. ¿Qué pasa si necesitamos más de una sola propiedad? ¿Qué pasa si también necesitamos trabajar con el estado? Si el método showMessage otro método y este método lee this.props.something o this.state.something , entonces volveremos a encontrar el mismo problema. Y para resolverlo, tendríamos que pasar this.props y this.state como argumentos a todos los métodos llamados desde showMessage .

Si es verdad hacerlo, destruirá todas las comodidades que ofrece el uso de componentes basados ​​en clases. Es difícil recordar que trabajar con métodos de esta manera es difícil, es difícil automatizar, como resultado, los desarrolladores a menudo, en lugar de usar métodos similares, están de acuerdo en que hay errores en sus proyectos.

Del mismo modo, incrustar el código de alert en handleClick no resuelve un problema más global. Necesitamos estructurar el código para que pueda dividirse en muchos métodos, pero también para que podamos leer las propiedades y el estado que corresponden a la operación de representación asociada con una llamada en particular. Este problema, por cierto, ni siquiera se aplica exclusivamente a React. Puede reproducirlo en cualquier biblioteca para desarrollar interfaces de usuario, que coloca datos en objetos mutables como this .

¿Quizás para resolver este problema, puede vincular métodos a this en el constructor?

 class ProfilePage extends React.Component { constructor(props) {   super(props);   this.showMessage = this.showMessage.bind(this);   this.handleClick = this.handleClick.bind(this); } showMessage() {   alert('Followed ' + this.props.user); } handleClick() {   setTimeout(this.showMessage, 3000); } render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

Pero esto no resuelve nuestro problema. Recuerde que es que estamos leyendo datos de this.props demasiado tarde, ¡y no en la sintaxis utilizada! Sin embargo, este problema se resolverá si confiamos en los cierres de JavaScript.

Los desarrolladores a menudo intentan evitar cierres, ya que no es fácil pensar en valores que, con el tiempo, no pueden mutar. ¡Pero las propiedades en React son inmutables! (O, como mínimo, esto es muy recomendable). Esto le permite dejar de percibir los cierres como algo por lo que el programador puede, como dicen, "pegarse un tiro en el pie".

Esto significa que si "bloquea" las propiedades o el estado de una operación de representación en particular en el cierre, siempre puede contar con que no cambien.

 class ProfilePage extends React.Component { render() {   //  !   const props = this.props;   //    ,      render.   //   -   .   const showMessage = () => {     alert('Followed ' + props.user);   };   const handleClick = () => {     setTimeout(showMessage, 3000);   };   return <button onClick={handleClick}>Follow</button>; } } 

Como puede ver, aquí "capturamos" las propiedades durante la llamada al método de render .


Propiedades capturadas por la llamada de render

Con este enfoque, se garantiza que cualquier código que se encuentre en el método de render (incluido showMessage ) verá las propiedades capturadas durante una llamada particular a este método. Como resultado, React ya no podrá evitar que hagamos lo que necesitamos.

En el método de render , puede describir tantas funciones auxiliares como desee y todas ellas podrán usar las propiedades y el estado "capturados". Así es como los cierres resolvieron nuestro problema.

Análisis de la solución del problema mediante cierre.


Lo que acabamos de llegar nos permite resolver el problema , pero ese código parece extraño. ¿Por qué se necesita una clase si las funciones se declaran dentro del método de render y no como métodos de clase?

De hecho, podemos simplificar este código deshaciéndonos del "shell" en la forma de una clase que lo rodea:

 function ProfilePage(props) { const showMessage = () => {   alert('Followed ' + props.user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Aquí, como en el ejemplo anterior, las propiedades se capturan en la función, ya que React se las pasa como un argumento. A diferencia de this , React nunca props objeto de props .

Esto se vuelve un poco más obvio si los props destruyen en la declaración de función:

 function ProfilePage({ user }) { const showMessage = () => {   alert('Followed ' + user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

Cuando el componente principal ProfilePage con otras propiedades, React volverá a llamar a la función ProfilePage . Pero el controlador de eventos que ya se ha llamado "pertenece" a la llamada anterior a esta función, esta llamada usa su propio valor de user y su propia devolución de showMessage showMessage, que lee este valor. Todo esto permanece intacto.

Es por eso que en la versión original de nuestro ejemplo, cuando se trabaja con un componente funcional, seleccionar otro perfil después de hacer clic en el botón correspondiente antes de que se muestre el mensaje no cambia nada. Si se seleccionó un perfil de Sophie antes de hacer clic en el botón, se mostrará 'Followed Sophie' en la ventana del mensaje, pase lo que pase.


Usando un componente funcional

Este comportamiento es correcto (por cierto, también puede inscribirse en Sunil ).

Ahora hemos descubierto cuál es la gran diferencia entre funciones y clases en React. Como ya se mencionó, estamos hablando del hecho de que los componentes funcionales capturan valores. Ahora hablemos de ganchos.

Ganchos


Cuando se usan ganchos, el principio de "capturar valores" se extiende al estado. Considere el siguiente ejemplo:

 function MessageThread() { const [message, setMessage] = useState(''); const showMessage = () => {   alert('You said: ' + message); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value); }; return (   <>     <input value={message} onChange={handleMessageChange} />     <button onClick={handleSendClick}>Send</button>   </> ); } 

Aquí puedes experimentar con él

Aunque este no es un ejemplo ejemplar de una interfaz de aplicación de mensajería, este proyecto ilustra la misma idea: si un usuario envió un mensaje, el componente no debe confundirse acerca de qué mensaje se envió. El message constante de este componente funcional captura el estado que "pertenece" al componente que hace que el navegador sea el controlador de clics para el botón que llama. Como resultado, el message almacena lo que estaba en el campo de entrada al momento de hacer clic en el botón Send .

El problema de capturar propiedades y estados por componentes funcionales


Sabemos que los componentes funcionales en React, por defecto, capturan propiedades y estados. Pero, ¿qué sucede si necesitamos leer los últimos datos de propiedades o estados que no pertenecen a una llamada de función en particular? ¿Qué pasa si queremos " leerlos del futuro "?

En componentes basados ​​en clases, esto podría hacerse simplemente haciendo referencia a this.props o this.state , ya que this es una entidad mutable. Su cambio se dedica a Reaccionar. Los componentes funcionales también pueden funcionar con valores mutables que comparten todos los componentes. Estos valores se denominan ref :

 function MyComponent() { const ref = useRef(null); //     `ref.current`. // ... } 

Sin embargo, el programador necesita gestionar dichos valores de forma independiente.

La esencia de ref juega el mismo papel que los campos de una instancia de una clase. Esta es una "salida de emergencia" en un mundo imperativo mutable. Puede que esté familiarizado con el concepto de referencias DOM, pero esta idea es mucho más general. Se puede comparar con una caja en la que un programador puede poner algo.

Incluso externamente, una construcción de la forma this.something parece una imagen especular de la construcción de this.something . Son una representación del mismo concepto.

De forma predeterminada, React no crea entidades de ref en los componentes funcionales para los valores de propiedad o estado más recientes. En muchos casos, no los necesitará, y su creación automática sería una pérdida de tiempo. Sin embargo, trabajar con ellos, si es necesario, se puede organizar por su cuenta:

 function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value);   latestMessage.current = e.target.value; }; 

Si leemos el message en showMessage , veremos el mensaje que estaba en el campo al momento de hacer clic en el botón Send . Pero si lee latestMessage.current , puede obtener el último valor, incluso si continuamos ingresando texto en el campo después de hacer clic en el botón Send .

Puede comparar este y estos ejemplos para evaluar independientemente la diferencia. El valor de ref es una forma de "evitar" la uniformidad del renderizado, en algunos casos puede ser muy útil.

En general, debe evitar leer o escribir valores de ref durante el proceso de representación porque estos valores son mutables. Nos esforzamos por hacer que la representación sea predecible. Sin embargo, si necesitamos obtener el valor más reciente de algo almacenado en propiedades o en estado, actualizar manualmente el valor de ref puede ser una tarea tediosa. Se puede automatizar utilizando el efecto:

 function MessageThread() { const [message, setMessage] = useState(''); //    . const latestMessage = useRef(''); useEffect(() => {   latestMessage.current = message; }); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; 

Aquí hay un ejemplo que usa este código

Estamos asignando un valor dentro del efecto, como resultado, el valor de ref solo cambiará después de que se actualice el DOM. Esto asegura que nuestra mutación no interrumpa características como Time Slicing y Suspense , que dependen de la continuidad de las operaciones de renderizado.

Usar el valor de ref de esta manera no se requiere a menudo. La captura de propiedades o estados generalmente parece ser un patrón mucho mejor del comportamiento estándar del sistema. Sin embargo, esto puede ser conveniente cuando se trabaja con API imperativas , como las que usan intervalos o suscripciones. Recuerde que puede trabajar de esta manera con cualquier valor: con propiedades, con variables almacenadas en estado, con todo el objeto de props o incluso con una función.

Este patrón, además, puede ser útil para fines de optimización. Por ejemplo, cuando algo como useCallback cambia con demasiada frecuencia. Es cierto que la solución preferida a menudo es usar un reductor .

Resumen


En este artículo, observamos uno de los patrones incorrectos para usar componentes basados ​​en clases y hablamos sobre cómo resolver este problema con los cierres. Sin embargo, puede notar que cuando intenta optimizar los enlaces especificando una serie de dependencias, puede encontrar errores relacionados con cierres obsoletos. ¿Significa esto que las fallas en sí mismas son un problema? No lo creo

Como se muestra arriba, los cierres, de hecho, nos ayudan a solucionar pequeños problemas que son difíciles de notar. Asimismo, facilitan la escritura de código que funciona correctamente en paralelo . Esto es posible debido al hecho de que dentro del componente las propiedades y el estado correctos con los que se procesó este componente están "bloqueados".

En todos los casos que he visto hasta ahora, el problema de los "cierres obsoletos" se produjo debido a la suposición errónea de que "las funciones no cambian", o que "las propiedades siempre son las mismas". Espero que después de leer este material, esté convencido de que esto no es así.

Las funciones "capturan" sus propiedades y estado, y por lo tanto, también es importante comprender qué funciones están en cuestión. Esto no es un error; es una característica de los componentes funcionales. Las funciones no deben excluirse de la "matriz de dependencias" para useEffect o useCalback , por ejemplo. (Una herramienta adecuada para resolver el problema suele ser useReducer o useRef . Hablamos de esto anteriormente, y pronto prepararemos materiales que se dedicarán a la elección de este o aquel enfoque).

Si la mayor parte del código en nuestras aplicaciones se basará en componentes funcionales, esto significa que necesitamos saber más sobre la optimización del código y qué valores pueden cambiar con el tiempo.

: « , , , , , ».

. , React , . , « », . , React .

, , .


React —

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


All Articles