React Hooks : useEffect

React Hooks : useEffect

Peticiones de datos, suscripciones y actualizaciones manuales del DOM en componentes de React serían ejemplos de efectos secundario...

Introducción

Interactuar con una API/base de datos, acceder a local storage o cambiar el DOM manualmente serán tareas con las que nos encontraremos a menudo en el desarrollo web con React.

A estas tareas se le llaman efectos secundarios o side effects, puesto que son tareas que se ejecutan fuera del dominio/ecosistema principal de React, que es lidiar con estados, props y la IU en general.

En general son tareas que necesitan ejecutarse después del renderizado de nuestra IU y/o después de cada actualización de estado.

Llamadas a API: ejemplo preliminar

A la hora de hacer llamadas a API, a priori podríamos pensar en programar un código como el de abajo, voy a explicar cómo lo haríamos y porqué no se debe hacer de esta forma.

import React from "react"

export default function App() {
    const [estado, setEstado] = React.useState({})

    fetch("https://swapi.dev/api/people/1")
    .then(res => res.json())
    .then(resParseada => setEstado(resParseada))


    return (
        <div>
            <pre>{JSON.stringify(estado, null, 2)}</pre>
        </div>
    )
}

Como podemos ver al usar console.log(estado), el problema que esto causa es que crea un loop infinito de renderizado del componente.

Una de las razones para que esto se produzca es que usamos fetch() en un nivel superior de nuestro componente. Cada vez que el componente se renderiza, llamará a fetch(),que a su vez actualizará nuestro estado con el resultado de la llamad a la API, lo que hace que React re-renderice el componente, lo cual volverá a llamar a fetch() y aquí se forma el ciclo infinito.

api call.gif

useEffect

React nos ofrece el método useEffect() que nos permite manejar efectos secundarios además de permitirnos sincronizarlos con el estado de React dado que podemos usarlo dentro de un componente.

No se necesita una API especial para acceder al estado o props, ya que se encuentran en el mismo ámbito (función/componente React). Los hooks aprovechan los closures de JavaScript y evitan introducir API's donde JavaScript ya ofrece una solución.

Por defecto, useEffect es ejecutado después de cada renderizado de IU y/o actualización de estado.

useEffect recibe dos parámetros:

  • Una función callback: normalmente dentro de esta función, ejecutamos código que interactuará con un efecto secundario(peticiones de datos, establecimiento de suscripciones y actualizaciones manuales del DOM etc..) para mantenerlo sincronizado con un estado local.
  • Array de dependencias: Este array contiene valores que si cambian de un renderizado a otro, hará que el efecto se ejecute, es decir, limita el numero de veces que se ejecuta el efecto y determina cuándo se ejecutará el efecto en vez de ejecutarlo después de cada actualización/renderizado. Si lo dejamos vacío, el efecto se ejecutará solamente una vez.

Miremos un ejemplo de useEffect:

import React from "react"

export default function App() {
    const [contador, setContador] = React.useState(0)

    console.log("Componente renderizado")

    // Efectos Secundarios
    React.useEffect(function() {
        console.log("Efecto ejecutado")
    }, [])

    return (
        <div>
            <h2>El contador es : {contador}</h2>
            <button onClick={() => setContador(contadorAnterior => contadorAnterior + 1)}>Incrementar</button>
        </div>
    )
}

useffect.gif

Como podemos observar, al dejar el array de dependencias vacío,console.log("Efecto ejecutado") se ejecuta sólo una vez cuando el componente carga por primera vez. Cuándo actualizamos contador haciendo clic en el botón y, como consecuencia causando un re-renderizado, este efecto ya no se ejecuta.

Esto se produce porque tenemos un array vacío, el cual no está buscando por cambios entre un renderizado y el siguiente.

Si quisiéramos ejecutar el efecto cada vez que la variable de estado contador cambie, debemos hacerle saber al efecto, que contador es una de las dependencias:

import React from "react"

export default function App() {
    const [contador, setContador] = React.useState(0)

    console.log("Componente renderizado")

    // Efectos Secundarios
    React.useEffect(function() {
        console.log("Efecto ejecutado")
    }, [contador])

    return (
        <div>
            <h2>El contador es : {contador}</h2>
            <button onClick={() => setContador(contadorAnterior => contadorAnterior + 1)}>Incrementar</button>
        </div>
    )
}

useffect dependency.gif

Llamadas a API: refactorizando nuestro ejemplo

Ahora, que ya tenemos los conocimientos suficientes, vamos a refactorizar el ejemplo que dimos al principio del artículo para evitar el loop infinito de renderizado.

Como sabemos, si proporcionamos un array de dependencias vacío como segundo argumento de nuestro método useEffect(), ejecutaremos el efecto una sola vez:

import React from "react"

export default function App() {
    const [estado, setEstado] = React.useState({})

    React.useEffect(() => {
        console.log("Efecto ejecutado");
        fetch("https://swapi.dev/api/people/1")
            .then(res => res.json())
            .then(resParseada => setEstado(resParseada))

    }, [])


    return (
        <div>
            <pre>{JSON.stringify(estado, null, 2)}</pre>
        </div>
    )
}

Además, como hemos señalado, useEffect también nos permite acceder a variables de estado, puesto que lo usamos en el mismo ámbito de componente que el estado/props.

import React from "react"

export default function App() {
    const [starWarsData, setStarWarsData] = React.useState({})
    const [contador, setContador] = React.useState(1)

    React.useEffect(function() {
        console.log("Efecto ejecutado")
        fetch(`https://swapi.dev/api/people/${contador}`)
            .then(res => res.json())
            .then(data => setStarWarsData(data))
    }, [contador])

    return (
        <div>
            <button onClick={() => setContador(prevCount => prevCount + 1)}>Proximo personaje</button>
            <pre>{JSON.stringify(starWarsData, null, 2)}</pre>
        </div>
    )
}

Aquí ejecutamos la llamada a la API de Star Wars, asignamos contador a la llamada HTTP y configuramos contador dentro del array de dependencias para que cada vez que cambie, vuelva a ejecutar una llamada y luego la respuesta se asigna al estado con setStarWarsData(data).

apicall hahaha.gif

Conclusión

¿Qué es un "efecto secundario" en React? ¿Cuáles son algunos ejemplos?

  • Cualquier código que interactúe con un "sistema exterior".
  • Almacenamiento local, API, websockets...

¿Qué NO es un "efecto secundario" en React? ¿Ejemplos?

  • Cualquier cosa de la que React esté a cargo
  • Mantener el estado, mantener la interfaz de usuario sincronizada con los datos, renderizar elementos DOM

¿Cuándo se ejecuta useEffect? ¿Cuándo NO funciona la función de efecto?

  • Tan pronto como se carga el componente (primer renderizado)
  • En cada nueva representación del componente (suponiendo que no haya un array de dependencias)
  • NO ejecutará el efecto cuando los valores de las dependencias en el array permanecen igual entre renderizados.

¿Cómo explicarías qué es el "array de dependencias"?

  • Segundo parámetro de la función useEffect
  • Una forma para que React sepa si debe volver a ejecutar la función de efecto

En mi siguiente artículo veremos qué son las fugas de memoria y cómo solucionarlas.