¿Revolución o dolor? Informe Yandex React Hooks

Mi nombre es Artyom Berezin, soy desarrollador de varios servicios internos de Yandex. Durante los últimos seis meses, he estado trabajando activamente con React Hooks. En el proceso, hubo algunas dificultades que tuvieron que ser combatidas. Ahora quiero compartir esta experiencia contigo. En el informe, examiné la API React Hook desde un punto de vista práctico: ¿por qué necesitamos ganchos? ¿Vale la pena cambiarlo? Es mejor tenerlo en cuenta al portarlo. Es fácil cometer errores durante la transición, pero evitarlos tampoco es tan difícil.



- Los ganchos son solo otra forma de describir la lógica de sus componentes. Le permite agregar a los componentes funcionales algunas características que anteriormente eran inherentes solo a los componentes de las clases.



Primero que nada, es soporte para el estado interno, luego soporte para efectos secundarios. Por ejemplo, solicitudes de red o solicitudes a WebSocket: suscripción, cancelación de suscripción de algunos canales. O, tal vez, estamos hablando de solicitudes a otras API de navegador asíncronas o síncronas. Además, los ganchos nos dan acceso al ciclo de vida del componente, a su inicio de vida, es decir, al montaje, a la actualización de sus accesorios y a su muerte.



Probablemente la forma más fácil de ilustrar en comparación. Aquí está el código más simple que solo puede estar con un componente en las clases. El componente está cambiando algo. Este es un contador regular que se puede aumentar o disminuir, solo un campo en estado. En general, creo que si está familiarizado con React, el código es completamente obvio para usted.



Un componente similar que realiza exactamente la misma función, pero escrito en ganchos, parece mucho más compacto. Según mis cálculos, en promedio, cuando se transfiere de componentes en clases a componentes en ganchos, el código disminuye aproximadamente una vez y media, y lo complace.

Algunas palabras sobre cómo funcionan los ganchos. Un enlace es una función global que se declara dentro de React y se llama cada vez que se representa un componente. React rastrea las llamadas a estas funciones y puede cambiar su comportamiento o decidir qué debe devolver.



Existen algunas restricciones en el uso de ganchos que los distinguen de las funciones ordinarias. En primer lugar, no se pueden usar en componentes de clases, solo se aplica dicha restricción porque no están creados para ellos, sino para componentes funcionales. No se pueden llamar ganchos dentro de funciones internas, dentro de bucles, condiciones. Solo en el primer nivel de anidamiento, dentro de las funciones del componente. React impone esta restricción para poder rastrear qué ganchos se llamaron. Y los apila en un cierto orden en su cerebro. Entonces, si este orden cambia repentinamente o desaparece algo, es posible que se produzcan errores complejos, evasivos y difíciles de depurar.

Pero si tiene una lógica bastante complicada y le gustaría usar, por ejemplo, ganchos dentro de ganchos, entonces lo más probable es que sea una señal de que debe hacer un gancho. Suponga que conecta varios ganchos entre sí en un gancho personalizado separado. Y dentro de él puede usar otros ganchos personalizados, creando así una jerarquía de ganchos, destacando la lógica general allí.



Los ganchos proporcionan algunas ventajas sobre las clases. En primer lugar, como se deduce del anterior, utilizando ganchos personalizados, puede buscar la lógica mucho más fácil. Anteriormente, utilizando el enfoque con componentes de orden superior, presentamos algún tipo de lógica compartida, y era una envoltura sobre el componente. Ahora ponemos esta lógica dentro de los ganchos. Por lo tanto, el árbol de componentes se reduce: su anidamiento se reduce, y React se vuelve más fácil para rastrear los cambios de componentes, recalcular el árbol, recalcular el DOM virtual, etc. Esto resuelve el problema del llamado infierno envoltorio. Los que trabajan con Redux, creo, están familiarizados con esto.

El código escrito usando ganchos es mucho más fácil de minimizar con minimizadores modernos como Terser o el antiguo UglifyJS. El hecho es que no necesitamos guardar los nombres de los métodos, no necesitamos pensar en prototipos. Después de la transpilación, si el objetivo es ES3 o ES5, generalmente obtenemos un montón de prototipos que parchan. Aquí no es necesario hacer todo esto, por lo tanto, es más fácil de minimizar. Y, como resultado de no usar clases, no necesitamos pensar en esto. Para los principiantes, este suele ser un gran problema y probablemente una de las principales razones de los errores: olvidamos que puede ser una ventana, que necesitamos vincular el método, por ejemplo, en el constructor o de alguna otra manera.

Además, el uso de ganchos le permite resaltar la lógica que controla cualquier efecto secundario. Anteriormente, esta lógica, especialmente cuando tenemos varios efectos secundarios para un componente, tenía que dividirse en diferentes métodos del ciclo de vida del componente. Y, desde que aparecieron los ganchos de minimización, apareció React.memo, ahora los componentes funcionales se prestan a la memorización, es decir, este componente no se recreará ni actualizará con nosotros si sus accesorios no han cambiado. Esto no se pudo hacer antes, ahora es posible. Todos los componentes funcionales se pueden envolver en memo. También dentro del gancho useMemo apareció, que podemos usar para calcular algunos valores pesados, o instanciar algunas clases de utilidad solo una vez.

El informe estará incompleto si no hablo de algunos ganchos básicos. En primer lugar, estos son ganchos de gestión estatal.



En primer lugar, use State.



Un ejemplo es similar al del principio del informe. useState es una función que toma un valor inicial y devuelve una tupla del valor actual y la función para cambiar ese valor. Toda la magia es servida por React internamente. Simplemente podemos leer este valor o cambiarlo.

A diferencia de las clases, podemos usar tantos objetos de estado como necesitemos, divide el estado en partes lógicas para no mezclarlos en un objeto, como en las clases. Y estas piezas estarán completamente aisladas unas de otras: se pueden cambiar independientemente una de la otra. El resultado, por ejemplo, de este código: cambiamos dos variables, calculamos el resultado y visualizamos botones que nos permiten cambiar la primera variable aquí y allá, y la segunda variable aquí y allá. Recuerde este ejemplo, porque más adelante haremos algo similar, pero mucho más complicado.



Hay un uso de StateState en esteroides para los amantes de Redux. Le permite cambiar el estado de manera más consistente utilizando un reductor. Creo que aquellos que están familiarizados con Redux ni siquiera pueden explicar, para aquellos que no están familiarizados, diré.

Un reductor es una función que acepta un estado y algún objeto, generalmente llamado acción, que describe cómo debería cambiar este estado. Más precisamente, pasa algunos parámetros, y dentro del reductor ya decide, dependiendo de sus parámetros, cómo cambiará el estado y, como resultado, se debe devolver un nuevo estado, actualizado.



Aproximadamente de esta manera se usa en el código del componente. Tenemos un gancho useReducer, toma una función reductora y el segundo parámetro es el valor inicial del estado. Devuelve, como useState, el estado actual y la función para cambiarlo es despachar. Si pasa un objeto de acción para despachar, invocaremos un cambio de estado.



Uso muy importante Efecto gancho. Le permite agregar efectos secundarios al componente, dando una alternativa al ciclo de vida. En este ejemplo, usamos un método simple con useEffect: solo solicita algunos datos del servidor, con la API, por ejemplo, y muestra estos datos en la página.



UseEffect tiene un modo avanzado, esto es cuando la función pasada a useEffect devuelve alguna otra función, luego se llamará a esta función en el próximo ciclo, cuando se aplicará este useEffect.

Olvidé mencionar que useEffect se llama de forma asincrónica, justo después de que el cambio se aplica al DOM. Es decir, garantiza que se ejecutará después de que se procese el componente, y puede conducir al siguiente renderizado si cambian algunos valores.



Aquí nos encontramos por primera vez con un concepto como las dependencias. Algunos ganchos, useEffect, useCallback, useMemo, toman una matriz de valores como segundo argumento, lo que nos permitirá decir qué rastrear. Los cambios en esta matriz conducen a algún tipo de efectos. Por ejemplo, aquí, hipotéticamente, tenemos algún tipo de componente para elegir un autor de alguna lista. Y un plato con libros de este autor. Y cuando el autor cambie, se llamará a useEffect. Cuando se cambia este authorId, se llamará a una solicitud y se cargarán los libros.

También menciono al pasar ganchos como useRef, esta es una alternativa a React.createRef, algo similar a useState, pero los cambios a ref no conducen a la representación. A veces conveniente para algunos hacks. useImperativeHandle nos permite declarar ciertos "métodos públicos" en el componente. Si usa useRef en el componente padre, puede extraer estos métodos. Para ser sincero, lo intenté una vez con fines educativos, en la práctica no fue útil. useContext es algo bueno, le permite tomar el valor actual del contexto si el proveedor ha definido este valor en algún lugar más alto en el nivel de jerarquía.

Hay una forma de optimizar las aplicaciones React en ganchos; esta es la memorización. La memorización se puede dividir en interna y externa. Primero sobre el exterior.



Esta es React.memo, prácticamente una alternativa a la clase React.PureComponent, que rastreó los cambios en los accesorios y los componentes cambiados solo cuando los accesorios o el estado cambiaron.

Aquí, una cosa similar, sin embargo, sin un estado. También supervisa los cambios en los accesorios, y si los accesorios han cambiado, se produce un renderizador. Si los accesorios no han cambiado, el componente no se actualiza y ahorramos en esto.



Métodos internos de optimización. En primer lugar, esto es algo de bajo nivel: useMemo, rara vez se usa. Le permite calcular algún valor y volver a calcularlo solo si los valores especificados en las dependencias han cambiado.



Hay un caso especial de useMemo para una función llamada useCallback. Se utiliza principalmente para memorizar el valor de las funciones del controlador de eventos que se pasarán a los componentes secundarios para que estos componentes secundarios no puedan representarse nuevamente. Se usa simplemente. Describimos una determinada función, la envolvemos en useCallback e indicamos de qué variables depende.

Mucha gente tiene una pregunta, pero ¿necesitamos esto? ¿Necesitamos ganchos? ¿Nos mudamos o nos quedamos como antes? No hay una respuesta única, todo depende de las preferencias. En primer lugar, si está directamente vinculado rígidamente a la programación orientada a objetos, si sus componentes, está acostumbrado a ellos como una clase, tienen métodos que pueden extraerse, entonces, probablemente, esto puede parecerle superfluo. En principio, cuando escuché por primera vez sobre ganchos, me pareció que era demasiado complicado, se estaba agregando algún tipo de magia, y no estaba claro por qué.

Para los amantes de las funcionalidades, esto es, digamos, imprescindible, porque los ganchos son funciones, y las técnicas de programación funcional les son aplicables. Por ejemplo, puede combinarlos o hacer cualquier cosa, utilizando, por ejemplo, bibliotecas como Ramda y similares.



Como nos deshicimos de las clases, ya no necesitamos vincular este contexto a los métodos. Si usa estos métodos como devoluciones de llamada. Por lo general, esto era un problema, porque tenía que recordar vincularlos en el constructor, o usar una extensión no oficial de la sintaxis del lenguaje, como las funciones de flecha como propiedad. Práctica bastante común. Usé mi decorador, que también es, en principio, experimentalmente, en métodos.



Hay una diferencia en cómo funciona el ciclo de vida, cómo administrarlo. Los ganchos asocian casi todas las acciones del ciclo de vida con el gancho useEffect, que le permite suscribirse tanto al nacimiento como a la actualización de un componente y a su muerte. En las clases, para esto, tuvimos que redefinir varios métodos, como componentDidMount, componentDidUpdate y componentWillUnmount. Además, el método shouldComponentUpdate ahora se puede reemplazar con React.memo.



Hay una diferencia bastante pequeña en cómo se maneja el estado. Primero, las clases tienen un objeto de estado. Tuvimos que meter algo allí. En ganchos, podemos dividir el estado lógico en algunas partes, lo que sería conveniente para nosotros operar por separado.

setState () de componentes en clases permite especificar un parche de estado, cambiando así uno o más campos del estado. En los ganchos, tenemos que cambiar todo el estado como un todo, y esto es incluso bueno, porque está de moda usar todo tipo de cosas inmutables y nunca esperar que nuestros objetos muten. Siempre son nuevos con nosotros.

La característica principal de las clases que los ganchos no tienen: podríamos suscribirnos a los cambios de estado. Es decir, cambiamos el estado e inmediatamente nos suscribimos a sus cambios, procesando imperativamente algo inmediatamente después de que se aplican los cambios. En ganchos, esto simplemente no funciona. Esto debe hacerse de una manera muy interesante, te diré más.

Y un poco sobre la forma funcional de actualización. Funciona tanto allí como allí, cuando las funciones de cambio de estado aceptan otra función, que este estado no debería cambiar, sino crear. Y si en el caso del componente de clase puede devolvernos algún tipo de parche, entonces en los ganchos debemos devolver todo el nuevo valor.

En general, es poco probable que obtenga una respuesta si se mueve o no. Pero le aconsejo que al menos intente, al menos para el nuevo código, sentirlo. Cuando recién comencé a trabajar con ganchos, inmediatamente identifiqué varios ganchos personalizados que son convenientes para mi proyecto. Básicamente, intenté reemplazar algunas de las características que había implementado a través de componentes de orden superior.



useDismounted: para aquellos que están familiarizados con RxJS, existe la oportunidad de darse de baja en masa de todos los Observables dentro de un componente, o dentro de una función, suscribiendo cada Observable a un objeto especial, Asunto, y cuando se cierra, todas las suscripciones se cancelan. Esto es muy conveniente si el componente es complejo, si hay muchas operaciones asincrónicas dentro del Observable, es conveniente darse de baja de todas a la vez, y no de cada una por separado.

useObservable devuelve un valor de Observable cuando aparece uno nuevo allí. Un gancho similar de useBehaviourSubject regresa de BehaviourSubject. Su diferencia con Observable es que inicialmente tiene algún significado.

El conveniente uso del gancho personalizadoDebouncedValue nos permite organizar, por ejemplo, un sujest para la cadena de búsqueda, de modo que no cada vez que presione una tecla, envíe algo al servidor, pero espere hasta que el usuario termine de escribir.

Dos ganchos similares. useWindowResize devuelve los valores actuales actuales para los tamaños de ventana. El siguiente gancho para la posición de desplazamiento es useWindowScroll. Los uso para contar algunas ventanas emergentes o ventanas modales, si hay cosas complicadas que simplemente no se pueden hacer con CSS.

Y un gancho tan pequeño para implementar teclas de acceso rápido, que el componente, cuando está presente en la página, se suscribe a alguna tecla de acceso rápido. Cuando muere, se produce una baja automática.

¿Para qué son convenientes estos ganchos personalizados? Que podemos anular una cancelación de suscripción dentro del gancho, y no tenemos que pensar en cancelar la suscripción manual en algún lugar del componente donde se usa este gancho.

No hace mucho tiempo, me lanzaron un enlace a la biblioteca de uso de reacción, y resultó que la mayoría de estos ganchos personalizados ya estaban implementados allí. Y escribí una bicicleta. Esto a veces es útil, pero en el futuro, lo más probable es que los tire y use react-use. Y le aconsejo que también vea si tiene la intención de usar ganchos.



En realidad, el objetivo principal del informe es mostrar cómo escribir incorrectamente, qué problemas pueden ser y cómo evitarlos. Lo primero, probablemente lo que cualquiera que esté estudiando estos ganchos y tratando de escribir algo, sea usar incorrectamente useEffect. Aquí está el código similar al que 100% todos escribieron si intentaron ganchos. Se debe al hecho de que useEffect se percibe inicialmente mentalmente, como una alternativa a componentDidMount. Pero, a diferencia de componentDidMount, que se llama solo una vez, useEffect se llama en cada render. Y el error aquí es que cambia, digamos, la variable de datos, y al mismo tiempo cambiarla conduce a un procesador de componentes, como resultado, el efecto se volverá a solicitar. Por lo tanto, obtenemos una serie interminable de solicitudes AJAX al servidor, y el componente en sí mismo se actualiza, actualiza, actualiza constantemente.



Arreglarlo es muy simple. Debe agregar aquí una matriz vacía de las dependencias de las que depende y los cambios en los que reiniciará el efecto. Si tenemos una lista vacía de dependencias especificadas aquí, entonces el efecto, en consecuencia, no se reiniciará. Este no es un tipo de pirateo, es una característica básica del uso de useEffect.



Digamos que lo arreglamos. Ahora un poco complicado. Tenemos un componente que representa algo que debe ser tomado del servidor para algún tipo de identificación. En este caso, en principio, todo funciona bien hasta que cambiemos el entityId en el padre, tal vez esto no sea relevante para su componente.



Pero lo más probable es que si cambia o es necesario cambiarlo, y si tiene un componente antiguo en su página y resulta que no se está actualizando, es mejor agregar entityId aquí, como una dependencia, causando la actualización, actualizando los datos.



Un ejemplo más complejo con useCallback. Aquí, a primera vista, todo está bien. Tenemos una página determinada que tiene algún tipo de temporizador de cuenta regresiva o, por el contrario, un temporizador que simplemente funciona. Y, por ejemplo, una lista de hosts, y encima hay filtros que le permiten filtrar esta lista de hosts. Bueno, el mantenimiento se ha agregado aquí solo para ilustrar un valor que cambia con frecuencia que se traduce en un renderizador.

, , maintenance , , , onChange. onChange, . , HostFilters - , , dropdown, . , . , .



onChange useCallback. , .

, . , , . Facebook, React. , , , , '. , , confusing .



? — , - , , , , , . .

, , , , , , . , Garbage Collector , . , , , , . , , , reducer, , . , .

, , . - , , setValue - , , setState . - useEffect.

useEffect - , - , , , useEffect. useEffect , . , , Backbone, : , , , - . , , - , . - . , , , , - . , , , , , , . .

, , . , , . , . , . , , , dropdown . , . dropdown pop-up, useWindowScroll, useWindowResize , . , , — , .

, , . , , , , , . , , , , , .



, «», . , , TypeScript . . , reducer Redux , action. , action , action. , , , .

. , action. , , IncrementA 0, 1, 2, . . , , , , . action action, - . UnionType “Action”, , , action. .

— . , initialState, . , - . TypeScript. . , typeState , initialState.



reducer. State, Action, : switch action.type. TypeScript UnionType: case, - , type. action .

, : , , . .



? , . . , reducer. , action creator , , dispatch.



extension Dev Tools. . .

, , . , , . useDebugValue , - Dev Tool. useConstants, - , loaded, , , .



— . , . , . , , , . , , — - , — .

. Facebook ESLint, . , , . , dependencies . , , , .

, , , - , . , , , . . , - - .

— , , - . , , . , , - . , . . :

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


All Articles