logo blog voltadev
Side Effects - useEffect

Side Effects - useEffect

- views - 06-06-2022

Hola bienvenidos al blog 😀 este post lo voy dividir en 2 para poder explicar a profundidad lo que es un side Effects y el hook useEffect

Efectos secundarios (Side Effects)

En programación, llamamos efectos secundarios a las modificaciones que alteran el estado de nuestro programa. Vamos a verlo en términos prácticos comparando dos funciones:

(x,y) => x + y

nombre = “”;
(value) => nombre = value;

Decimos que la primera función no produce efectos secundarios, porque la ejecución de la misma no altera nada fuera del alcance de esta función. Podemos ejecutar esta función cuantas veces queramos y nada cambiará.


Por otro lado, la segunda función cambia una variable fuera de la ejecución de la función, alterando el estado de la App. Este es un efecto secundario.


Cuando hablamos de React, si el componente ejecuta una operación que altera el estado global de la app, estaríamos produciendo un efecto secundario. En general, un componente debe hacer operaciones que alteren al componente mismo, y no más. Por supuesto que hay muchas excepciones, sin embargo, hay que tener en cuenta que el código que no produce efectos secundarios es menos impredecible y más fácil de debuggear.


Algunos ejemplos de efectos secundarios en un componente pueden ser: realizar peticiones a un servidor con AJAX, alterar el DOM manualmente, conectarse a una websocket, etc.


En un componente funcional, estas operaciones no se pueden ejecutar, ya que las funciones de un componente, no producen efectos secundarios.


Para poder ejecutar operaciones que produzcan efectos secundarios, podemos usar el hook useEffect.


useEffect nos permite enviar una función que se ejecutará luego del render de una función. Esta función puede producir efectos secundarios, de ahí el nombre del hook useEffect.


En términos prácticos, useEffect es el lugar perfecto para:

  • Ejecutar código como parte del ciclo de vida del componente
  • Hacer peticiones AJAX
  • Actualizar el DOM directamente, por ejemplo para reproducir un vídeo
  • Logging de cambios

useEffect recibe como segundo argumento que es un arreglo. En este arreglo pueden pasar variables que se usarán para determinar si el efecto debe ejecutarse o no. En la documentación de React podemos ver un muy buen ejemplo:

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // Solo se ejecuta si count cambió entre un render y otro

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

Cuando pasamos un arreglo vacío, useEffect sólo se ejecutará una vez luego del primer render.

Por último, si una función se retorna del efecto, ésta se ejecutará luego del último render una vez que el componente desaparezca de la interfaz.

Side Effects y useEffect 😎 ¿ Cuándo se ejecuta ?

El principal objetivo de react es proporcionar la mejor experiencia de usuario posible y para eso hay que ser capaces de distinguir entre lo que es "importante y lo que es necesario" entre lo prioritario y lo que puede esperar.


"La prioridad máxima de una aplicación" es reaccionar ante las acciones del usuario y proporcionar una respuesta visual rápida y efectiva, esta respuesta visual implica realizar cambios sobre nuestra interfaz y para hacerlo sabemos que tenemos que provocar un renderizado que al final terminará de ACTUALIZAR EL DOM, pero esto no es lo único que debe hacer una aplicación


Podemos establecer una premisa, que la prioridad máxima cuando el usuario realiza una búsqueda, es ofrecer esta respuesta oficial inmediata mostrando este spiner.


spinner


A continuación y como consecuencia de esta busqueda hay varias tareas adicionales que nuestra App debe realizar, que si bien no son tan importantes, en esta primera estapa son completamente necesarias, para que funcione completamente. Ejemplos de estas tareas:


  • añadir un query string a esta URL -> Cambiar URL
  • cambiar el título de la pestaña -> Cambiar título
  • realizar la petición a la API para obtener los resultados -> API de búsqueda
  • registrar las métricas en nuestro servidor

Consecuencias useEffect


Desde el punto de vista del usuario todas estas acciones son secundarias.


Desde del punto de vista del desarrollador son efectos secundarios (side effects) de la acción principal, para poder gestionarlos correctamente. React nos proporciona el Hook de useEffect "hook para efectos secundarios" pero antes de ver como funciona hay que entender a que nos referimos cuando hablamos de side effects.

Side effects

Consecuencia de otras acciones -> en este caso como consecuencia de un cambio de estado


  • ☑️ Se ejecuta siempre después de la acción principal -> en este caso después del renderizado
  • ☑️ El orden en que se ejecutan es irrelevante
  • ☑️ Implicaciones externas

En este caso podemos actualizar:


  • título
  • URL
  • llamada a la API

Si es al revés su orden, el resultado final sería el mismo y tendría implicaciones externas a la propia función que los define, mientras que un renderizado debería afectar sólo al nodo que se renderiza o cualquiera de sus hijos. Un side effects puede afectar a elementos externos al propio DOM -> como son el document title, la location href o a la propia red que utilizamos para la llamada a la API


📚 side effects es el cambio de un estado principal

Distinguir side effects

  • Definir la tarea que vamos a realizar -> por ejemplo realizar esta búsqueda
  • Detectar las acciones -> en que se divide esta tarea. Podemos considerar una acción como el mínimo de conjuntos de operaciones necesarias para poder producir un resultado relevante. Cambiar el valor de una variable es una operación, pero no produce un resultado relevante por sí misma. Mientras que modificar el título del documento o modificar la url, aunque también son simples operaciones, producen un resultado relevante. Eso sería la diferencia entre una operación y una acción como tal, que puede incluir una o varias operaciones.

Podemos entender estas acciones también como las distintas responsabilidades que existen dentro de la tarea


  • Elegir la acción principal -> una vez tengamos esta acción principal
  • El resto de acciones serán side effects (acción del usuario en la UI)

👀 Lo más importante que hay que entender, es elegir cuál es la acción principal, implica tomar una decisión y que no siempre existe una única respuesta, En muchos casos la acción principal estará totalmente clara pero en otros sera algo completamente debatible y tendrá que determinarse en base a lo que nosotros consideramos más prioritario. Como en todo tenemos que tomar una decisión y en base a ella actuar en consecuencia.


import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
          document.title = count + 1;
        }}
      >
        + 1
      </button>
    </div>
  );
};

export default App;

Vamos a ver como podemos conseguir que el valor de este contador cambie el titulo de pestaña para indicar el valor, aquí es donde tenemos que comenzar a analizar. Tenemos que comenzar a pensar en esa tarea, cual sería la tarea para incrementar ese contador y de esa tarea tenemos varias acciones.


  • este setCount sería una acción que produce algún resultado relevante -> si actualiza la interfaz
  • este document.title = count + 1 es una acción o no -> produce otro resultado relevante, en este caso actualizar el título de la pestaña

Por lo tanto aquí tenemos una tarea que se divide en 2 acciones, ahora cual es la acción principal que terminamos aquí, Por un lado tenemos el setCount que es actualizar el state y este se renderiza en pantalla. Aquí tenemos la respuesta visual más inmediata del usuario. Donde el usuario va a centrar su foco de atención, el document.title no tiene mucho sentido si actualizo la app, actualiza la pestaña una vez si quito el setCount y a partir de aquí no tiene mucho sentido, entonces podemos decir claramente que de las 2, la acción principal seria setCount y document.title = count + 1 sería un side effect.

Sabiendo que es un side effects cual es problema de gestionarlo de esta manera y porque no debemos hacerlo. Esto se ve claramente cuando comenzamos a añadir mas cosas a esta aplicación.

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
          document.title = count + 1;
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
          setCount(count + 2);
          document.title = count + 2;
        }}
      >
        + 2
      </button>
    </div>
  );
};

export default App;

Como ven aquí lo que estoy haciendo es traer toda esta acción principal, que evidentemente no puedo quitarla de aquí. El boton tiene que tener un acción principal, pero cada vez que modifico este contador tengo que venir acompañando con su side effects asociado, es decir yo modifico este contador -> en el momento que yo haga este setCount tengo que venir detrás con el sideEffect y si hago este setCount en 10 sitios, tengo que asegurarme que esté sideEffects y todos los demás que tenga asociado se ejecutan siempre detrás y eso es algo que nos puede inducir a un error.


😀 Actualizo y todo bien


setcount2

¿ Pero qué pasa que por alguna acción casual hago un setCount y se me olvida poner este document.title ? Si algún punto se nos olvida hacer esta pequeña modificación, el reultado sería una App con un state inconsistente.


Si aplico el botón mas 1 -> tengo el 1 en la pestaña Pero si aplico el botón +2 -> solamente actualiza el contador y no hago el document.title


Ademas tenemos otro punto en contra con estos side effects: hasta que no termina toda esta acción el renderizado no se produce. Es decir este render no se inicia cuando hacemos setCount -> si no que tiene que esperar a este document.title en este caso es una operación muy sencilla.


Imagina que esto tarda medio segundo, estaríamos bloqueando ese renderizado medio segundo, hasta que termine de hacer todo, lo cual no tiene sentido. Podemos priorizar la acción principal y dejar que se produzca el renderizado, posteriormente cuando acabe el renderizado ya realizar todos esos sideEffects, porque no nos importa ni el orden en que se ejecuten, ni nos importa exactamente el orden en que lo haga después de la acción principal y antes de la siguiente nos sirve perfectamente y da igual si es en medio segundo antes o medio segundo después, no hay ningún problema y para eso tenemos el hook de useEffects.


⚓ useEffect

👁️ Al ser un hook, tiene que llamarse siempre dentro de un componente el primer parámetro que recibe este useEffect:


  • es una función -> **se ejecutará siempre después del renderizado y tendrá los valores actualizados

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = count; // este count ya está actualizado
  });

  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
          setCount(count + 2);
        }}
      >
        + 2
      </button>
    </div>
  );
};

export default App;

ahora lo que conseguimos es que cambie el estado de count


useEffect(() => {
  document.title = count; // este count ya esta actualizado
});

si lo hacemos con tanto con un +1 o un +2, siempre que cambie el state de count, da igual que lo hagamos desde un botón, desde otro o inclusive desde un tercero, siempre provocaremos un side effects que consiga exactamente esto

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = count; // este count ya esta actualizado
    console.log(count);
  });

  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
          setCount(count + 2);
        }}
      >
        + 2
      </button>
      <button
        onClick={() => {
          setCount(count + 3);
        }}
      >
        + 3
      </button>
    </div>
  );
};

export default App;

setcount3

Le doy +1,+2,+3


Lo que acabamos de hacer es vincular este count a un side effects y nos da igual donde se ejecuta este count, nos da igual que si lo seteamos 1 2 o 7 veces da igual, no hay que preocuparse por esos side effects, vamos hacer un console.log(count) entendiéndolo como un pequeño side effects. No se habla de sideEffects porque no debería haber un console.log en producción, pero si ciertamente sería como un pequeño side effects para explicar este ejemplo.

Ahora lo que tenemos que entender ¿ Cuando se ejecuta este useEffects y por qué funciona así ? ¿ por qué tiene los valores actualizados de {count} ?

Vamos a ir analizando con los console.log() que es lo que sucede

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  console.log("%c 1: render antes useEffect ", "color: MediumSpringGreen");

  useEffect(() => {
    console.log("useEffect", count);
    document.title = count; // este count ya esta actualizado
  });

  console.log("%c 2: render después useEffect ", "color: LightCoral");

  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
          setCount(count + 2);
        }}
      >
        + 2
      </button>
      <button
        onClick={() => {
          setCount(count + 3);
        }}
      >
        + 3
      </button>
    </div>
  );
};

export default App;

Cuando nosotros carguemos nuestra App por primera vez, inicialmente lo que vamos a provocar es el primer renderizado. Haremos este renderizado

console.log("%c 1: render antes useEffect ", "color: MediumSpringGreen");

//luego llegaramos a este useEffect
useEffect(() => {
  console.log("useEffect", count);
  document.title = count; // este cout ya está actualizado
});

//finalmente haremos este render después
console.log("%c 2: render después useEffect ", "color: LightCoral");

Lo cierto es que estamos definiendo una función pero no la estamos ejecutando. Si se fijas le estamos pasando a useEffect una función, no le estamos pasando su ejecución entonces lo que va a pasar:


1. Primero renderiza este console.log() antes del useEffect

console.log("%c 1: render antes useEffect ", "color: MediumSpringGreen");

2. Vera este useEffect y dira de acuerdo me quedo con esta función, para ejecutarla después porque es un side effects

useEffect(() => {
  console.log("useEffect", count);
  document.title = count; // este cout ya esta actualizado
});

3. Posteriormente seguirá haciendo todo el render o finalmente retornará un valor

console.log("%c 2: render después useEffect ", "color: LightCoral");

  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        + 1
      </button>
      <button
        onClick={() => {
          setCount(count + 2);
        }}
      >
        + 2
      </button>
      <button
        onClick={() => {
          setCount(count + 3);
        }}
      >
        + 3
      </button>
    </div>
  );
};

export default App;

Cuando retorne se pintará en nuestra pantalla los botones de la interfaz, el count 0, +1,+2 y +3 el renderizado ya ha terminado.


Lo siguiente que hace React cuando termina el renderizado, es venir y decir ¿ durante este renderizado hay que ejecutar algún useEffect ? la respuesta es que si, entonces ejecuta este useEffect ante que se produzca el siguiente renderizado y de esta manera podemos verlo en consola.


console


Cada vez que provoquemos un renderizado esto volverá a ser así.


console2


useEffect se ejecuta de forma asíncrona, es decir, "no es inmediatamente" después del render de forma síncrona, si no que es de forma asíncrona y que implica que no podemos saber el momento exacto cuando se ejecuta, podemos aproximarlo porque sabemos que es después del render, pero no sabemos si es 10ms, 12ms etc.

No es como useLayout que ese si se ejecuta después del render forma síncrona, esto no nos importa, pero tiene sentido a la hora de hablar de métricas, podemos ver que hay un pequeño delay entre que se ejecuta y termina useEffect.


useEffect sabemos que se ejecuta después de un render y se ejecuta ante del siguiente, eso nos garantiza react

Por eso es que este useEffect se ejecuta en el primer renderizado y por eso aquí aparece un 0. En este caso podríamos evitar que se ejecute en primer renderizado utilizando un sencillo if

useEffect(() => {
  if (count === 0) return; //la fn() se ejecuta pero no produce resultado
  console.log("useEffect", count);
  document.title = count; // esté count ya esta actualizado
});

setcount2

Por supuesto necesitamos mucho mayor control de cuando se debe ejecutar un useEffect o no. Eso lo podemos hacer con las dependencias.

💻 Code Example useEffect

link code

¡Muchas gracias por tu lectura dejo el link de cafecito apoyo voluntario, tu contribución me motiva a seguir creando contenido de alta calidad. ¡Muchas gracias por tu apoyo!


Buy Me a Coffee at ko-fi.com