Sincronización con Efectos
Algunos componentes necesitan sincronizarse con sistemas externos. Por ejemplo, es posible que desees controlar un componente que no es de React basándote en el estado de React, configurar una conexión con un servidor o enviar un registro de análisis cuando un componente aparece en pantalla.Los Efectoste permiten ejecutar algún código después del renderizado para que puedas sincronizar tu componente con algún sistema fuera de React.
Aprenderás
- Qué son los Efectos
- En qué se diferencian los Efectos de los eventos
- Cómo declarar un Efecto en tu componente
- Cómo evitar volver a ejecutar un Efecto innecesariamente
- Por qué los Efectos se ejecutan dos veces en desarrollo y cómo solucionarlo
¿Qué son los Efectos y en qué se diferencian de los eventos?
Antes de abordar los Efectos, debes familiarizarte con dos tipos de lógica dentro de los componentes de React:
- Código de renderizado(presentado enDescribir la interfaz de usuario) vive en el nivel superior de tu componente. Aquí es donde tomas las props y el estado, los transformas y devuelves el JSX que quieres ver en pantalla.El código de renderizado debe ser puro.Como una fórmula matemática, solo debecalcularel resultado, pero no hacer nada más.
- Manejadores de eventos(presentados enAgregar interactividad) son funciones anidadas dentro de tus componentes quehacencosas en lugar de solo calcularlas. Un manejador de eventos podría actualizar un campo de entrada, enviar una solicitud HTTP POST para comprar un producto o navegar al usuario a otra pantalla. Los manejadores de eventos contienen"efectos secundarios"(cambian el estado del programa) causados por una acción específica del usuario (por ejemplo, un clic en un botón o escribir).
A veces esto no es suficiente. Considera un componenteChatRoomque debe conectarse al servidor de chat siempre que esté visible en pantalla. Conectarse a un servidor no es un cálculo puro (es un efecto secundario), por lo que no puede ocurrir durante el renderizado. Sin embargo, no hay un evento particular único, como un clic, que haga queChatRoomse muestre.
Los Efectoste permiten especificar efectos secundarios que son causados por el renderizado en sí, en lugar de por un evento particular.Enviar un mensaje en el chat es uneventoporque es causado directamente por el usuario al hacer clic en un botón específico. Sin embargo, establecer una conexión con el servidor es unEfectoporque debería ocurrir sin importar qué interacción causó que el componente apareciera. Los Efectos se ejecutan al final de uncommitdespués de que la pantalla se actualiza. Este es un buen momento para sincronizar los componentes de React con algún sistema externo (como la red o una biblioteca de terceros).
Nota
Aquí y más adelante en este texto, "Efecto" con mayúscula se refiere a la definición específica de React mencionada anteriormente, es decir, un efecto secundario causado por el renderizado. Para referirnos al concepto de programación más amplio, diremos "efecto secundario".
Puede que no necesites un Efecto
No te apresures a agregar Efectos a tus componentes.Ten en cuenta que los Efectos se usan típicamente para "salir" de tu código de React y sincronizarse con algún sistemaexterno. Esto incluye APIs del navegador, widgets de terceros, red, etc. Si tu Efecto solo ajusta algún estado basado en otro estado,puede que no necesites un Efecto.
Cómo escribir un Efecto
Para escribir un Efecto, sigue estos tres pasos:
- Declara un Efecto.Por defecto, tu Efecto se ejecutará después de cadacommit.
- Especifica las dependencias del Efecto.La mayoría de los Efectos solo deben volver a ejecutarsecuando sea necesarioen lugar de después de cada renderizado. Por ejemplo, una animación de aparición gradual solo debe activarse cuando un componente aparece. Conectarse y desconectarse de una sala de chat solo debe ocurrir cuando el componente aparece y desaparece, o cuando la sala de chat cambia. Aprenderás a controlar esto especificandodependencias.
- Agrega limpieza si es necesario.Algunos Efectos necesitan especificar cómo detener, deshacer o limpiar lo que estaban haciendo. Por ejemplo, "conectar" necesita "desconectar", "suscribirse" necesita "cancelar suscripción" y "fetch" necesita "cancelar" o "ignorar". Aprenderás a hacer esto devolviendo unafunción de limpieza.
Veamos cada uno de estos pasos en detalle.
Paso 1: Declarar un Efecto
Para declarar un Efecto en tu componente, importa elHook useEffectde React:
Luego, llámalo en el nivel superior de tu componente y coloca algún código dentro de tu Efecto:
Cada vez que tu componente se renderiza, React actualizará la pantallay luegoejecutará el código dentro deuseEffect. En otras palabras,useEffect“retrasa” la ejecución de un fragmento de código hasta que ese renderizado se refleje en la pantalla.
Veamos cómo puedes usar un Efecto para sincronizarte con un sistema externo. Considera un componente React<VideoPlayer>. Sería útil controlar si está reproduciendo o pausado pasándole una propisPlaying:
Tu componente personalizadoVideoPlayerrenderiza la etiqueta<video>integrada del navegador:
Sin embargo, la etiqueta<video>del navegador no tiene una propisPlaying. La única forma de controlarla es llamando manualmente a los métodosplay() y pause()en el elemento DOM.Necesitas sincronizar el valor de la propisPlaying, que indica si el vídeodeberíaestar reproduciéndose actualmente, con llamadas comoplay() y pause().
Primero necesitaremosobtener una refal nodo DOM de<video>.
Podrías sentirte tentado a intentar llamar aplay() o pause()durante el renderizado, pero eso no es correcto:
La razón por la que este código no es correcto es que intenta hacer algo con el nodo DOM durante el renderizado. En React,el renderizado debe ser un cálculo purode JSX y no debe contener efectos secundarios como modificar el DOM.
Además, cuandoVideoPlayerse llama por primera vez, ¡su DOM aún no existe! No hay un nodo DOM todavía para llamar aplay() o pause(), porque React no sabe qué DOM crear hasta que devuelves el JSX.
La solución aquí esenvolver el efecto secundario conuseEffectpara sacarlo del cálculo de renderizado:
Al envolver la actualización del DOM en un Efecto, permites que React actualice primero la pantalla. Luego se ejecuta tu Efecto.
Cuando tu componenteVideoPlayerse renderiza (ya sea la primera vez o si se vuelve a renderizar), sucederán algunas cosas. Primero, React actualizará la pantalla, asegurándose de que la etiqueta<video>esté en el DOM con las props correctas. Luego React ejecutará tu Efecto. Finalmente, tu Efecto llamará aplay() o pause()dependiendo del valor deisPlaying.
Presiona Reproducir/Pausa varias veces y observa cómo el reproductor de video se mantiene sincronizado con el valor deisPlaying:
En este ejemplo, el "sistema externo" con el que sincronizaste el estado de React fue la API multimedia del navegador. Puedes usar un enfoque similar para envolver código heredado que no sea de React (como plugins de jQuery) en componentes declarativos de React.
Ten en cuenta que controlar un reproductor de video es mucho más complejo en la práctica. Llamar aplay()puede fallar, el usuario podría reproducir o pausar usando los controles integrados del navegador, y así sucesivamente. Este ejemplo está muy simplificado e incompleto.
Precaución
Por defecto, los Efectos se ejecutan después decadarenderizado. Por eso, un código como esteproducirá un bucle infinito:
Los Efectos se ejecutan comoresultadode un renderizado. Establecer el estadodesencadenaun renderizado. Establecer el estado inmediatamente en un Efecto es como conectar un enchufe a sí mismo. El Efecto se ejecuta, establece el estado, lo que provoca un re-renderizado, lo que hace que el Efecto se ejecute, establece el estado de nuevo, esto provoca otro re-renderizado, y así sucesivamente.
Los Efectos normalmente deberían sincronizar tus componentes con un sistemaexterno. Si no hay un sistema externo y solo quieres ajustar algún estado basándote en otro estado,es posible que no necesites un Efecto.
Paso 2: Especificar las dependencias del Efecto
Por defecto, los Efectos se ejecutan después decadarenderizado. A menudo, estono es lo que quieres:
- A veces, es lento. Sincronizar con un sistema externo no siempre es instantáneo, por lo que quizás quieras omitirlo a menos que sea necesario. Por ejemplo, no quieres reconectarte al servidor de chat con cada pulsación de tecla.
- A veces, es incorrecto. Por ejemplo, no quieres desencadenar una animación de fundido de entrada del componente con cada pulsación de tecla. La animación solo debería reproducirse una vez, cuando el componente aparece por primera vez.
Para demostrar el problema, aquí está el ejemplo anterior con algunas llamadas aconsole.logy una entrada de texto que actualiza el estado del componente padre. Observa cómo escribir hace que el Efecto se vuelva a ejecutar:
Puedes indicarle a React queomita volver a ejecutar el Efecto innecesariamenteespecificando un array dedependenciascomo segundo argumento de la llamada auseEffect. Comienza añadiendo un array vacío[]al ejemplo anterior en la línea 14:
Deberías ver un error que diceReact Hook useEffect has a missing dependency: 'isPlaying':
El problema es que el código dentro de tu Efectodepende dela propisPlayingpara decidir qué hacer, pero esta dependencia no fue declarada explícitamente. Para solucionar este problema, añadeisPlayingal array de dependencias:
Ahora todas las dependencias están declaradas, así que no hay error. Especificar[isPlaying]como el array de dependencias le dice a React que debe omitir volver a ejecutar tu Efecto siisPlayinges el mismo que durante el renderizado anterior. Con este cambio, escribir en la entrada no hace que el E
El array de dependencias puede contener múltiples dependencias. React solo omitirá volver a ejecutar el Efecto sitodaslas dependencias que especifiques tienen exactamente los mismos valores que tenían durante el renderizado anterior. React compara los valores de las dependencias usando la comparaciónObject.is. Consulta lareferencia de useEffectpara más detalles.
Observa que no puedes "elegir" tus dependencias.Obtendrás un error de lint si las dependencias que especificaste no coinciden con lo que React espera basándose en el código dentro de tu Efecto. Esto ayuda a detectar muchos errores en tu código. Si no quieres que algún código se vuelva a ejecutar,edita el código del Efecto para que no "necesite" esa dependencia.
Precaución
Los comportamientos sin el array de dependencias y con un array de dependenciasvacío[]son diferentes:
Examinaremos de cerca qué significa "montaje" en el siguiente paso.
Paso 3: Agrega limpieza si es necesario
Considera un ejemplo diferente. Estás escribiendo un componenteChatRoomque necesita conectarse al servidor de chat cuando aparece. Se te proporciona una APIcreateConnection()que devuelve un objeto con los métodosconnect() y disconnect(). ¿Cómo mantienes el componente conectado mientras se muestra al usuario?
Comienza escribiendo la lógica del Efecto:
Sería lento conectarse al chat después de cada re-renderizado, así que agregas el array de dependencias:
El código dentro del Efecto no usa ninguna prop o estado, por lo que tu array de dependencias es[](vacío). Esto le dice a React que solo ejecute este código cuando el componente se "monte", es decir, aparezca en la pantalla por primera vez.
Intentemos ejecutar este código:
Este Efecto solo se ejecuta al montar, por lo que podrías esperar que"✅ Connecting..."se imprima una vez en la consola.Sin embargo, si revisas la consola,"✅ Connecting..."se imprime dos veces. ¿Por qué sucede esto?
Imagina que el componenteChatRoomes parte de una aplicación más grande con muchas pantallas diferentes. El usuario comienza su recorrido en la páginaChatRoom. El componente se monta y llama aconnection.connect(). Luego imagina que el usuario navega a otra pantalla, por ejemplo, a la página de Configuración. El componenteChatRoomse desmonta. Finalmente, el usuario hace clic en Atrás yChatRoomse monta de nuevo. Esto establecería una segunda conexión, ¡pero la primera conexión nunca se destruyó! A medida que el usuario navega por la aplicación, las conexiones seguirían acumulándose.
Errores como este son fáciles de pasar por alto sin pruebas manuales exhaustivas. Para ayudarte a detectarlos rápidamente, en desarrollo React vuelve a montar cada componente una vez inmediatamente después de su montaje inicial.
Ver el registro"✅ Connecting..."dos veces te ayuda a notar el problema real: tu código no cierra la conexión cuando el componente se desmonta.
Para solucionar el problema, devuelve unafunción de limpiezadesde tu Efecto:
React llamará a tu función de limpieza cada vez antes de que el Efecto se ejecute nuevamente, y una última vez cuando el componente se desmonte (se elimine). Veamos qué sucede cuando se implementa la función de limpieza:
Ahora obtienes tres registros en la consola en desarrollo:
"✅ Connecting...""❌ Disconnected.""✅ Connecting..."
Este es el comportamiento correcto en desarrollo.Al volver a montar tu componente, React verifica que navegar fuera y volver no rompería tu código. ¡Desconectar y luego conectar nuevamente es exactamente lo que debería suceder! Cuando implementas bien la limpieza, no debería haber una diferencia visible para el usuario entre ejecutar el Efecto una vez versus ejecutarlo, limpiarlo y ejecutarlo nuevamente. Hay un par extra de llamadas de conexión/desconexión porque React está sondeando tu código en busca de errores en desarrollo. Esto es normal, ¡no intentes hacer que desaparezca!
En producción, solo verías"✅ Connecting..."impreso una vez.El remontaje de componentes solo ocurre en desarrollo para ayudarte a encontrar Efectos que necesitan limpieza. Puedes desactivar elModo Estrictopara optar por no participar en el comportamiento de desarrollo, pero recomendamos mantenerlo activado. Esto te permite encontrar muchos errores como el anterior.
¿Cómo manejar que el Efecto se dispare dos veces en desarrollo?
React remonta intencionalmente tus componentes en desarrollo para encontrar errores como en el último ejemplo.La pregunta correcta no es "cómo ejecutar un Efecto una vez", sino "cómo arreglar mi Efecto para que funcione después de remontarse".
Por lo general, la respuesta es implementar la función de limpieza. La función de limpieza debe detener o deshacer lo que sea que el Efecto estaba haciendo. La regla general es que el usuario no debería poder distinguir entre el Efecto ejecutándose una vez (como en producción) y una secuencia deconfiguración → limpieza → configuración(como verías en desarrollo).
La mayoría de los Efectos que escribirás encajarán en uno de los patrones comunes a continuación.
Precaución
No uses refs para evitar que los Efectos se disparen
Una trampa común para evitar que los Efectos se disparen dos veces en desarrollo es usar unarefpara evitar que el Efecto se ejecute más de una vez. Por ejemplo, podrías "arreglar" el error anterior con unuseRef:
Esto hace que solo veas"✅ Connecting..."una vez en desarrollo, pero no soluciona el error.
Cuando el usuario navega fuera, la conexión aún no se cierra y cuando navega de regreso, se crea una nueva conexión. A medida que el usuario navega por la aplicación, las conexiones seguirían acumulándose, igual que antes del "arreglo".
Para solucionar el error, no es suficiente con hacer que el Efecto se ejecute una vez. El efecto necesita funcionar después de volver a montarse, lo que significa que la conexión necesita limpiarse como en la solución anterior.
Consulta los ejemplos a continuación para ver cómo manejar patrones comunes.
Controlar widgets que no son de React
A veces necesitas agregar widgets de interfaz de usuario que no están escritos en React. Por ejemplo, digamos que estás agregando un componente de mapa a tu página. Tiene un métodosetZoomLevel()y te gustaría mantener el nivel de zoom sincronizado con una variable de estadozoomLevelen tu código React. Tu Efecto se vería similar a esto:
Ten en cuenta que en este caso no se necesita limpieza. En desarrollo, React llamará al Efecto dos veces, pero esto no es un problema porque llamar asetZoomLeveldos veces con el mismo valor no hace nada. Puede ser un poco más lento, pero esto no importa porque no se volverá a montar innecesariamente en producción.
Algunas API pueden no permitir llamarlas dos veces seguidas. Por ejemplo, el métodoshowModaldel elemento integrado<dialog>lanza un error si lo llamas dos veces. Implementa la función de limpieza y haz que cierre el diálogo:
En desarrollo, tu Efecto llamará ashowModal(), luego inmediatamente aclose(), y luego ashowModal()de nuevo. Esto tiene el mismo comportamiento visible para el usuario que llamar ashowModal()una vez, como verías en producción.
Suscribirse a eventos
Si tu Efecto se suscribe a algo, la función de limpieza debería cancelar la suscripción:
En desarrollo, tu Efecto llamará aaddEventListener(), luego inmediatamente aremoveEventListener(), y luego aaddEventListener()de nuevo con el mismo manejador. Así que solo habría una suscripción activa a la vez. Esto tiene el mismo comportamiento visible para el usuario que llamar aaddEventListener()una vez, como en producción.
Desencadenar animaciones
Si tu Efecto anima algo, la función de limpieza debería restablecer la animación a los valores iniciales:
En desarrollo, la opacidad se establecerá en1, luego en0, y luego en1de nuevo. Esto debería tener el mismo comportamiento visible para el usuario que establecerla en1directamente, que es lo que sucedería en producción. Si usas una biblioteca de animaciones de terceros que admita interpolación, tu función de limpieza debería restablecer la línea de tiempo a su estado inicial.
Obtener datos
Si tu Efecto obtiene algo, la función de limpieza deberíaabortar la obtencióno ignorar su resultado:
No puedes "deshacer" una solicitud de red que ya ocurrió, pero tu función de limpieza debe asegurar que la obtención queya no es relevanteno siga afectando tu aplicación. Si eluserIdcambia de'Alice' a 'Bob', la limpieza asegura que la respuesta de'Alice'se ignore incluso si llega después de'Bob'.
En desarrollo, verás dos obtenciones en la pestaña Red.No hay nada malo en eso. Con el enfoque anterior, el primer Efecto se limpiará inmediatamente, por lo que su copia de la variableignorese establecerá entrue. Así que, aunque haya una solicitud extra, no afectará al estado gracias a la verificaciónif (!ignore).
En producción, solo habrá una solicitud.Si la segunda solicitud en desarrollo te molesta, el mejor enfoque es usar una solución que deduplica solicitudes y almacena en caché sus respuestas entre componentes:
Esto no solo mejorará la experiencia de desarrollo, sino que también hará que tu aplicación se sienta más rápida. Por ejemplo, el usuario que presiona el botón Atrás no tendrá que esperar a que se carguen algunos datos nuevamente porque estarán en caché. Puedes construir tal caché tú mismo o usar una de las muchas alternativas a la obtención manual en Efectos.
Envío de análisis
Considera este código que envía un evento de análisis al visitar la página:
En desarrollo,logVisitse llamará dos veces para cada URL, por lo que podrías sentir la tentación de intentar solucionarlo.Recomendamos mantener este código tal como está.Al igual que con los ejemplos anteriores, no hay diferenciavisible para el usuarioentre ejecutarlo una vez y ejecutarlo dos veces. Desde un punto de vista práctico,logVisitno debería hacer nada en desarrollo porque no quieres que los registros de las máquinas de desarrollo sesguen las métricas de producción. Tu componente se vuelve a montar cada vez que guardas su archivo, por lo que de todos modos registra visitas adicionales en desarrollo.
En producción, no habrá registros de visitas duplicados.
Para depurar los eventos de análisis que estás enviando, puedes desplegar tu aplicación en un entorno de pruebas (que se ejecuta en modo de producción) o optar temporalmente por no usarModo Estrictoy sus comprobaciones de remontaje solo para desarrollo. También puedes enviar análisis desde los controladores de eventos de cambio de ruta en lugar de desde Efectos. Para análisis más precisos, losobservadores de intersecciónpueden ayudar a rastrear qué componentes están en el viewport y cuánto tiempo permanecen visibles.
No es un Efecto: Inicializar la aplicación
Alguna lógica solo debe ejecutarse una vez cuando la aplicación se inicia. Puedes ponerla fuera de tus componentes:
Esto garantiza que dicha lógica solo se ejecute una vez después de que el navegador cargue la página.
No es un Efecto: Comprar un producto
A veces, incluso si escribes una función de limpieza, no hay forma de evitar las consecuencias visibles para el usuario de ejecutar el Efecto dos veces. Por ejemplo, tal vez tu Efecto envía una solicitud POST como comprar un producto:
No querrías comprar el producto dos veces. Sin embargo, esta es también la razón por la que no deberías poner esta lógica en un Efecto. ¿Qué pasa si el usuario va a otra página y luego presiona Atrás? Tu Efecto se ejecutaría de nuevo. No quieres comprar el producto cuando el usuariovisitauna página; quieres comprarlo cuando el usuariohace clicen el botón Comprar.
Esto ilustra que si el desmontaje y montaje rompe la lógica de tu aplicación, esto generalmente revela errores existentes.Desde la perspectiva del usuario, visitar una página no debería ser diferente de visitarla, hacer clic en un enlace y luego presionar Atrás para ver la página nuevamente. React verifica que tus componentes cumplan con este principio desmontándolos y montándolos una vez en desarrollo.
Resumiendo todo
Este entorno de pruebas puede ayudarte a "entender" cómo funcionan los Efectos en la práctica.
Este ejemplo usasetTimeoutpara programar un registro en la consola con el texto de entrada que aparecerá tres segundos después de que se ejecute el Efecto. La función de limpieza cancela el tiempo de espera pendiente. Comienza presionando "Montar el componente":
Verás tres registros al principio:Schedule "a" log,Cancel "a" log y Schedule "a" logde nuevo. Tres segundos después también habrá un registro que dicea. Como aprendiste antes, el par extra de programar/cancelar se debe a que React desmonta y monta el componente una vez en desarrollo para verificar que hayas implementado bien la limpieza.
Ahora edita la entrada para que digaabc. Si lo haces lo suficientemente rápido, verásSchedule "ab" loginmediatamente seguido porCancel "ab" log y Schedule "abc" log.React siempre limpia el Efecto del renderizado anterior antes del Efecto del siguiente renderizado.Por eso, incluso si escribes en la entrada rápidamente, hay como máximo un tiempo de espera programado a la vez. Edita la entrada varias veces y observa la consola para entender cómo se limpian los Efectos.
Escribe algo en la entrada y luego presiona inmediatamente "Desmontar el componente". Observa cómo el desmontaje limpia el Efecto del último renderizado. Aquí, borra el último tiempo de espera antes de que tenga la oportunidad de ejecutarse.
Finalmente, edita el componente anterior y comenta la función de limpieza para que los tiempos de espera no se cancelen. Intenta escribirabcderápidamente. ¿Qué esperas que suceda en tres segundos? ¿console.log(text)dentro del tiempo de espera imprimirá elúltimotexty producirá cinco registros deabcde? ¡Pruébalo para comprobar tu intuición!
Tres segundos después, deberías ver una secuencia de registros (a,ab,abc,abcd y abcde) en lugar de cinco registros deabcde. Cada Efecto "captura" el valor detextde su renderizado correspondiente.No importa que el estado detexthaya cambiado: un Efecto del renderizado context = 'ab'siempre verá'ab'. En otras palabras, los Efectos de cada renderizado están aislados entre sí. Si tienes curiosidad sobre cómo funciona esto, puedes leer sobrecierres.
Resumen
- A diferencia de los eventos, los Efectos son causados por el renderizado en sí, no por una interacción específica.
- Los Efectos te permiten sincronizar un componente con algún sistema externo (API de terceros, red, etc.).
- Por defecto, los Efectos se ejecutan después de cada renderizado (incluido el inicial).
- React omitirá el Efecto si todas sus dependencias tienen los mismos valores que durante el último renderizado.
- No puedes "elegir" tus dependencias. Están determinadas por el código dentro del Efecto.
- Un array de dependencias vacío (
[]) corresponde al "montaje" del componente, es decir, cuando se añade a la pantalla. - En Modo Estricto, React monta los componentes dos veces (¡solo en desarrollo!) para probar tus Efectos bajo estrés.
- Si tu Efecto falla debido al remontaje, necesitas implementar una función de limpieza.
- React llamará a tu función de limpieza antes de que el Efecto se ejecute la próxima vez, y durante el desmontaje.
Try out some challenges
Challenge 1 of 4:Focus a field on mount #
In this example, the form renders a <MyInput /> component.
Use the input’s focus() method to make MyInput automatically focus when it appears on the screen. There is already a commented out implementation, but it doesn’t quite work. Figure out why it doesn’t work, and fix it. (If you’re familiar with the autoFocus attribute, pretend that it does not exist: we are reimplementing the same functionality from scratch.)
To verify that your solution works, press “Show form” and verify that the input receives focus (becomes highlighted and the cursor is placed inside). Press “Hide form” and “Show form” again. Verify the input is highlighted again.
MyInput should only focus on mount rather than after every render. To verify that the behavior is right, press “Show form” and then repeatedly press the “Make it uppercase” checkbox. Clicking the checkbox should not focus the input above it.
