useContext y useReducer. ¿Cómo replicar redux?

useContext y useReducer. ¿Cómo replicar redux?

Estos dos React hooks se complementan a la hora de crear aplicaciones. Si tu aplicación no es tan grande y compleja, estos dos react hooks te ayudaran a manejar el estado global de una forma muy similar a como lo hace Redux.

Si no estás familiarizado con Redux, te recomiendo revisar los principios usados en Redux. Además la combinación de useContext y useReducer es un buen punto de partida para empezar a entender como funciona.

Primero aprenderemos a utilizar useContext, luego useReducer, y finalmente uniremos estos dos React Hooks para replicar el comportamiento básico de Redux y controlar el estado global de una pequeña aplicación.

Contexto

La idea del contexto es que ahí guardes cierta información que necesitas en varios componentes. El problema a resolver es que esos componentes se encuentran en diferentes niveles del árbol de nodos de tu aplicación. Por esta razón, una instancia de un contexto contiene dos propiedades componentes, un Proveedor y un Consumidor (Provider, Consumer). Como consecuencia el proveedor almacena y suministra los datos, mientras el consumidor pide acceso a ellos.

UnContexto = React.createContext({});
// UnContexto = { Provider, Consumer }
....
<UnContexto.Provider>
<UnContexto.Consumer>

Agregando a useContext, existen tres formas de consumir el contexto actual de una aplicación

  1. static contextType = UnContexto
  2. <UnContexto.Consumer>
  3. useContext(UnContexto)

Las tres maneras de obtener el contexto depende de que exista su correspondiente componente proveedor padre <UnContexto.Provider>. De las tres, la forma más simple es useContext(UnContexto), punto a favor de los React Hooks.

A continuación vamos a describir el ejemplo para la creación del contexto, más tres ejemplos de los modos de consumirlo.

createContext(defaultContext)

Primero necesitamos crear un contexto para poder consumirlo, la forma de crear el contexto no cambia para los consumidores.

Creamos un nuevo proyecto en codesandbox.io y creamos un archivo llamado UserNameContext.js con el siguiente contenido. En el ejemplo queremos obtener del nombre de un usuario en nuestra aplicación, que se usa en distintos componentes.

import { createContext } from 'react';
const UserNameContext = createContext('');
export default UserNameContext

Ahora en nuestro archivo App.js importamos el contexto.

import UserNameContext from './UserNameContext';

Necesitamos suministrar a la aplicación con los datos de nuestro contexto, para eso ocupamos al componente <UserNameContext.Provider> y actualizamos el valor del contexto cuando el usuario escribe en el campo de texto.

import { useState } from 'react';
import Header from './components/Header';
import UserNameContext from './UserNameContext';
function App() {
  const [userName, setUserName] = useState('');
  return (
    <UserNameContext.Provider value={userName}>
      <h1>static contextType</h1>
      <Header />
      <input type="text" value={userName} onChange={e => setUserName(e.target.value)}/>
    </UserNameContext.Provider>
  );
}
export default App;

Para que quede claro la utilidad del contexto, creamos dos componentes, <Header> y <UserInfo> dentro de la carpeta llamada components.

import UserInfo from './UserInfo';
export default function Header() {
  return (
    <header>
      <h2>Header</h2>
      <UserInfo />
    </header>
  );
}

El componente <Header> es padre del componente <UserInfo>, de esta manera se ejemplifica el consumo del contexto en un tercer nivel de componentes.

El componente de <UserInfo /> contendrá al consumidor del contexto. Así que este es el único componente que cambiara su implementación para ejemplificar los tres modos de consumo del contexto. En las siguientes tres secciones se muestra cada forma.

static contextType = UnContexto

Para usar esta forma de consumir el contexto, debemos crear un componente de clase. La importación del contexto debe aplicarse también para los otros dos formas. Es algo que no cambia en las tres implementaciones.

import { Component } from 'react';
import UserNameContext from '../UserNameContext';
export default class UserInfo extends Component {
  static contextType = UserNameContext;
  render() {
    const value = this.context;
    return (
      <p>{value}</p>
    );
  }
}

Simplemente se agrega una propiedad estática contextType a la clase, asignándole como valor el objeto contexto generado por createContext(initialContext) anteriormente.

Luego podemos usar this.context en cualquiera de los métodos del ciclo de vida de un componente en React, para hacer referencia al valor actual del contexto y utilizarlo. Tal cual como se muestra en el ejemplo de arriba, donde utilizamos el método render().

<UnContexto.Consumer>

Esta manera de consumir el valor del contexto, pude usarse en un componente de clase o en un componente funcional, necesitamos importar el objeto del contexto y utilizar el componente Consumer que está definido como propiedad del objeto contexto.

Al componente Consumer mencionado en el párrafo anterior le pasamos como children una función, la cual puede recibir como primer parámetro el valor actual del contexto. Por último en este ejemplo mostramos el valor del contexto en una etiqueta <p>.

import UserNameContext from '../UserNameContext';
export default function UserInfo() {
  return (
    <UserNameContext.Consumer>
      {value => (<p>{value}</p>)}
    </UserNameContext.Consumer>
  );
}

useContext(UnContexto)

Uno de los objetivos de esta publicación es mostrar como usar el React Hook useContext(UnContexto), este hook es el equivalente a la propiedad static contextTypes = UnContexto en un componente de clase o el uso del componente <UnContexto.Consumer>, es decir, también puede obtener el valor actual del contexto y subscribirse a cambios del mismo.

import { useContext } from 'react';
import UserNameContext from '../UserNameContext';
export default function UserInfo() {
  const value  = useContext(UserNameContext);
  return (
    <p>{value}</p>
  );
}

Igual que las demás formas, importamos el objeto contexto, luego dentro del componente funcional, invocamos a useContext pasándole como parámetro el propio contexto. Así obtenemos el valor actual del contexto y lo guardamos en la constante value. Si el contexto cambia, esto provocara que el componente <UserInfo /> se vuelva a renderizar con el nuevo valor del contexto.

Compara la complejidad de obtener el contexto con useContext(UserNameContext), static contextType = UserNameContext y <UserNameContext.Consumer>.

Aquí puedes ver el ejemplo completo:

Edit useContext(UserNameContext)

useReducer

Este React Hook es bastante interesante como alternativa a useState, se recomienda usarlo cuando:

  • Necesitas controlar el estado en algún objeto complejo, lo más común es en un objeto literal o Arreglo
  • Realizar lógica compleja relacionada con el estado previo para calcular el próximo estado.

useReducer, como su nombre lo indica utiliza una función reducer, esta función es la que se encarga de manejar la complejidad de los cambios en el estado de la misma forma que los reducers en Redux.

Sintaxis

La sintaxis es la siguiente

const [state, dispatch] = useReducer(reducerFn, initialState, initLazyFn)
  1. reducerFn, es la función reducer encargada de la lógica de actualizar el estado, es una función en la forma (state, action) => { }.
  2. initialState, es el estado inicial.
  3. initLazyFn, este parámetro es opcional, se usa para calcular el estado inicial final basado en el initialState, se invoca initLazyFn(initialState) para obtener el estado final. Útil para separar la lógica del cálculo del estado inicial de la función reducerFn.

Ejemplo

Creamos un nuevo proyecto en codesandbox.io, y en el archivo App.js ponemos lo siguiente:

import { useReducer } from "react";
import INITIAL_STATE from "./initialState";
import parseInitialState from "./parseInitialState";
import { reduceUser } from "./reducer";
import "./styles.css";
export default function App() {
  const [state, dispatch] = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  function saveUser(e) {
    e.preventDefault();
    saveUserToLocalStorage(state);
  }
  return (
    <div className="App">
      <h1>useReducer(reducerFn, initState, parseInitialState)</h1>
  	  <form>
       ...
      </form>
    </div>
  );
}

El ejemplo es un formulario donde se guarda el nombre, apellido y edad de un usuario conforme escribe en sus respectivos inputs. Luego hasta el final tiene un botón para guardar al usuario en locaStorage al enviar el formulario.

Se tiene en archivos diferentes a initialState, initLazyFn (parseInitialState) y reduceFn(reduceUser).

initialState e initiLazyFn

initialState.js, como ya mencionamos es el estado inicial.

export default {
  name: "",
  age: 0,
  lastName: ""
};

parseInitialState.js (initLazyFn), dado que al enviar el formulario se guarda en el localStorage, se debe parsear los datos del localStorage si es que existen.

export default function (initialState) {
  const user = localStorage.getItem("user");
  if (user) {
    return parseJson(user);
  }
  return initialState;
}
function parseJson(json) {
  try {
    return JSON.parse(json);
  } catch (error) {
    console.log(error);
  }
}

Función reducer

reduceUser.js, la mayoría de las acciones realmente no son tan complejas, pero tampoco tan simples para usarlo en un useState(). El usuario guardado se puede almacenar en un arreglo de usuarios y que sea parte del estado, sin embargo no queremos complicar más las cosas, además necesitaríamos crear un componente que liste los usuarios.

export function reduceUser(state, action) {
  switch (action.type) {
    case "updateName":
      return { ...state, name: action.payload };
    case "updateLastName":
      return { ...state, lastName: action.payload };
    case "updateAge":
      return { ...state, age: action.payload };
    default:
      return state;
  }
}

Ahora bien, ya que tenemos todo el código de los archivos separados, podemos revisar el archivo App.js. En el componente App, en lugar de usar useState, se invoca a useReducer.

const [state, dispatch] = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
);

La invocación nos regresa un par de valores, el estado generado y una función dispatch, para mandar a ejecutar las acciones.

Por último nos faltan los campos para los datos del usuario y como se disparan las acciones.

import { useReducer } from "react";
import INITIAL_STATE from "./initialState";
import parseInitialState from "./parseInitialState";
import { reduceUser } from "./reducer";
import "./styles.css";
export default function App() {
  const [state, dispatch] = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  function saveUser(e) {
    e.preventDefault();
    saveUserToLocalStorage(state);
  }
  return (
    <div className="App">
      <h1>useReducer(reducerFn, initState, parseInitialState)</h1>
      <form onSubmit={saveUser}>
        <label>
          Nombre:
          <input
            type="text"
            value={state.name}
            onChange={(e) =>
              dispatch({ type: "updateName", payload: e.target.value })
            }
          />
        </label>
        <label>
          Apellidos:
          <input
            type="text"
            value={state.lastName}
            onChange={(e) =>
              dispatch({ type: "updateLastName", payload: e.target.value })
            }
          />
        </label>
        <label>
          Edad:
          <input
            type="number"
            value={state.age}
            onChange={(e) =>
              dispatch({ type: "updateAge", payload: e.target.value })
            }
          />
        </label>
        <button>Guardar en localStorage</button>
      </form>
      {JSON.stringify(state)}
    </div>
  );
}
function saveUserToLocalStorage(state) {
  try {
    localStorage.setItem("user", JSON.stringify(state));
  } catch (error) {
    console.log(error);
  }
}

Se utiliza el evento onChange para disparar acciones con dispatch() y el evento onSubmit en el formulario para guardar en localStorage (“saveUser”). Aquí abajo el ejemplo completo.

Edit useReducer(reducer, initState, initLazyFn)

Preparando codigo para combinar useContext y useReducer

Vamos a mejorar el ejemplo anterior, siguiendo las pautas que nos indica Redux, agregaremos todo lo relacionado con el estado en una carpeta llamada store.

Primero definimos el nombre de funciones como constantes en el archivo /store/actionTypes.js, para hacer referencia a ellas sin algún error de escribirlo incorrectamente.

actionTypes.js

export const UPDATE_NAME = "updateName";
export const UPDATE_LASTNAME = "upateLastName";
export const UPDATE_AGE = "updateAge";

actions.js

Segundo, definimos nuestras acciones como funciones que regresan el objeto necesario para la funcion reduceUser. En /store/actions.js

import { UPDATE_NAME, UPDATE_LASTNAME, UPDATE_AGE } from "./actionTypes";
export const updateName = (name) => ({ type: UPDATE_NAME, payload: name });
export const updateLastName = (lastName) => ({
  type: UPDATE_LASTNAME,
  payload: lastName
});
export const updateAge = (age) => ({ type: UPDATE_AGE, payload: age });

Función reducerUser

Tercero, actualizamos la función reduceUser con los nuevos identificadores para las acciones. En el archivo /store/reduceUser.js

import { UPDATE_NAME, UPDATE_LASTNAME, UPDATE_AGE } from "./actionTypes";
export function reduceUser(state, action) {
  const { type, payload } = action;
  switch (type) {
    case UPDATE_NAME:
      return { ...state, name: payload };
    case UPDATE_LASTNAME:
      return { ...state, lastName: payload };
    case UPDATE_AGE:
      return { ...state, age: payload };
    default:
      return state;
  }
}

Uniendo todo en App.js

Y finalmente vamos a actualizar las importaciones y las invocamos las funciones actions en App.js. Esto último para obtener el objeto action que necesita reduceUser(state, action) y tener un código más corto y entendible. Al menos en este caso nos evitamos la notación dispatch({ type: 'updateLastName', payload: e.target.value }). Ahora solo hacemos dispatch(updateLastName(e.target.value))

import { useReducer } from "react";
import INITIAL_STATE from "./store/initialState";
import parseInitialState from "./store/parseInitialState";
import { reduceUser } from "./store/reduceUser";
import { updateName, updateLastName, updateAge } from "./store/actions";
import "./styles.css";
export default function App() {
  const [state, dispatch] = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  function saveUser(e) {
    e.preventDefault();
    saveUserToLocalStorage(state);
  }
  return (
    <div className="App">
      <h1>useReducer(reducerFn, initState, initLazyFn)</h1>
      <form onSubmit={saveUser}>
        <label>
          Nombre:
          <input
            type="text"
            value={state.name}
            onChange={(e) => dispatch(updateName(e.target.value))}
          />
        </label>
        <label>
          Apellidos:
          <input
            type="text"
            value={state.lastName}
            onChange={(e) => dispatch(updateLastName(e.target.value))}
          />
        </label>
        <label>
          Edad:
          <input
            type="number"
            value={state.age}
            onChange={(e) => dispatch(updateAge(e.target.value))}
          />
        </label>
        <button>Guardar en localStorage</button>
      </form>
      {JSON.stringify(state)}
    </div>
  );
}
function saveUserToLocalStorage(state) {
  try {
    localStorage.setItem("user", JSON.stringify(state));
  } catch (error) {
    console.log(error);
  }
}

Mini redux con useContext y useReducer

En esta sección vamos a utilizar useContext y useReducer para crear una pequeña aplicación al estilo Redux.

Primero vamos a dividir nuestra pequeña aplicación en varios componentes, agregaremos un componente <Header> el cual contendrá a otro llamado <UserInfo>. Luego otro componente <UserForm> donde usaremos el contexto para usar las acciones de nuestro reducer y el estado.

Header y UserInfo

Creamos una carpeta llamada components y creamos tres archivos, Header.js, UserInfo.js y UserForm.js.

Archivo /components/Header.js.

import UserInfo from './UserInfo';
export default function Header() {
  return (
    <header>
      <h2>Header</h2>
      <UserInfo />
    </header>
  );
}

Archivo /components/UserInfo.js

import { useContext } from 'react';
import UserContext from '../store/UserContext';
export default function UserInfo() {
  const [state]  = useContext(UserContext);
  return (
    <>
      <p>Nombre: {state.name}</p>
      <p>Apellidos: {state.lastName}</p>
      <p>Edad: {state.age}</p>
    </>
  );
}

En el componente <UserInfo>, a diferencia de la primera versión que hicimos al inicio de esta publicación, el valor del contexto es un arreglo conteniendo state y dispatch. Solo que en <UserInfo> solo nos interesa state.

Antes de crear el componente <UserForm>, vamos a crear el contexto que se utilizara en <UserInfo> y en <UserForm>.

Crear contexto

Creamos el archivo /store/UserContext.js dentro de la carpeta store

import { createContext } from 'react';
const UserContext = createContext([]);
export default UserContext;

Como vemos el contexto contiene un arreglo. De momento vacío.

Uso de useContext y useReducer para crear el store

Luego en App.js utilizamos este contexto para que todos los componentes hijos puedan tener acceso.

import { useReducer } from "react";
import INITIAL_STATE from "./store/initialState";
import parseInitialState from "./store/parseInitialState";
import { reduceUser } from "./store/reduceUser";
import UserContext from './store/UserContext';
import "./styles.css";
export default function App() {
  const [state, dispatch] = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  return (
  	<UserContext.Provider value={[state, dispatch]}>
      <div className="App">
        <h1>useReducer(reducerFn, initState, initLazyFn)</h1>
        <Header />
        ....
      </div>
    </UserContext.Provider>
  );
}

Como vemos agregamos el componente <Provider> del objeto UserContext y le asignamos como valor un arreglo conteniendo state y el dispatch. Si queremos podemos pasar solo una variable, así podemos decir que el valor del contexto es el store, en realidad esto es igual a como lo hace Redux, solo que redux trae su propio Provider.

export default function App() {
  const store = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  return (
  	<UserContext.Provider value={store}>
      <div className="App">
        <h1>useReducer(reducerFn, initState, initLazyFn)</h1>
        <Header />
        ....
      </div>
    </UserContext.Provider>
  );
}

Usando useContext para usar store en UserForm

Ahora si podemos crear el componente <UserForm> que utilizara el contexto y disparará las acciones a nuestra función reduceUser, es decir, utilizara el store de la aplicación.

Archivo /components/UserForm.js. Simplemente copiamos el contenido JSX del formulario que estaba App.js, importamos los action creators y el UserContext para utilizar useContext(UserContext) y obtener el store.

import { useContext } from "react";
import { updateName, updateLastName, updateAge } from "../store/actions";
import UserContext from "../store/UserContext";
export default function UserForm() {
  const [state, dispatch] = useContext(UserContext);
  function saveUser(e) {
    e.preventDefault();
    saveUserToLocalStorage(state);
  }
  return (
    <form onSubmit={saveUser}>
      <label>
        Nombre:
        <input
          type="text"
          value={state.name}
          onChange={(e) => dispatch(updateName(e.target.value))}
        />
      </label>
      <label>
        Apellidos:
        <input
          type="text"
          value={state.lastName}
          onChange={(e) => dispatch(updateLastName(e.target.value))}
        />
      </label>
      <label>
        Edad:
        <input
          type="number"
          value={state.age}
          onChange={(e) => dispatch(updateAge(e.target.value))}
        />
      </label>
      <button>Guardar en localStorage</button>
    </form>
  );
}
function saveUserToLocalStorage(state) {
  try {
    localStorage.setItem("user", JSON.stringify(state));
  } catch (error) {
    console.log(error);
  }
}

Agregar <UserForm>

Finalmente podemos usar la etiqueta <UserForm> en nuestro archivo App.js

import { useReducer } from "react";
import INITIAL_STATE from "./store/initialState";
import parseInitialState from "./store/parseInitialState";
import { reduceUser } from "./store/reduceUser";
import UserContext from './store/UserContext';
import Header from './components/Header';
import UserForm from './components/UserForm';
import "./styles.css";
export default function App() {
  const store = useReducer(
    reduceUser,
    INITIAL_STATE,
    parseInitialState
  );
  return (
    <UserContext.Provider value={storre}>
      <div className="App">
        <h1>useReducer(reducerFn, initState, initLazyFn)</h1>
        <Header />
        <UserForm />
      </div>
    </UserContext.Provider>
  );
}

Listo podemos tener nuestra aplicación con un store igual al de Redux sin importar esta librería, esta funcionalidad ya viene construida dentro de React.

Aquí puedes ver el ejemplo completo.

Edit useContext y useReducer, replicando Redux

Conclusiones

Por si no sabias como usar el contexto, en esta publicación aprendimos como crearlo y suministrar su valor a cualquier elemento hijo del árbol de nodos. Después para consumir el valor utilizamos las tres formas de hacerlo:

  1. static contextType = UnContexto
  2. <UnContexto.Consumer>
  3. useContext(UnContexto)

Al final utilizamos el React Hook useContext y comparamos las complejidades, por consiguiente se puede notar que el useContext es la forma más simple de obtener el valor del contexto.

Aprendimos a utilizar useReducer() y como su funcionalidad es casi idéntica a los reducers de Redux. Luego mejoramos el código para separar la función reducer, los actions y los action types tal cual se haría en Redux separando responsabilidades.

Finalmente usamos createContext, useContext y useReducer para tener acceso al store de la misma forma que lo hacemos con Redux. Concluyendo que sin la necesidad de una librería de terceros, como lo es Redux, React ya contiene este tipo de funcionalidad gracias a los React Hooks useContext y useReducer. Estos hooks son ideales para aplicaciones pequeñas que no necesiten de las funcionalidades avanzadas de Redux.

Referencias

https://reactjs.org/docs/context.htm

https://reactjs.org/docs/hooks-reference.html#usecontext

https://reactjs.org/docs/hooks-reference.html#usereducer

React Hooks Personalizados con useEffect

React Hooks Personalizados con useEffect

Ya se explicó como usar useEffect en esta publicación, sin embargo aún queda una variante muy importante que de hecho es la variante más compleja, por esta razón merece ser parte de este contenido.

Inicialmente crearemos un ejemplo con esta forma de useEffect, la cual tendrá una función de retorno que sirve para hacer limpieza, llamaremos limpieza a esta función a lo largo de todo el contenido. Adicionalmente, crearemos dos React hooks personalizados para extraer la funcionalidad de la variante limpieza en piezas más pequeñas.

Como recordatorio, useEffect nos permite ejecutar y controlar efectos colaterales, como pueden ser peticiones a servicios, subscribirse a eventos, modificar el DOM o cualquier funcionalidad de manera imperativa.

Recordando el flujo de useEffect(effectFn, [deps])

Primero, la función effecFn (el primer parámetro que recibe useEffect) se ejecuta después de que el navegador ya ha pintado el componente en pantalla por primera vez (montar). Segundo, la función effectFn se ejecuta después de cada posterior repintado (actualizar). Estos dos comportamientos descritos tienen el mismo propósito que los métodos componentDidMount y componentDidUpdate.

El tercer propósito en useEffect se le llama limpieza, el cual lo podemos comparar con componentDidUnmount. En el primer pintado (montar) la función de limpieza no se ejecuta, solo se ejecuta en la fase de actualizar. Esto se debe a que la función limpieza, en realidad no existe aún en el primer pintado porque es el valor de retorno de effectFn.

El flujo de useEffect se representa en la imagen de abajo. Modificado de este original.

Flujo useEffect
Flujo useEffect

Haciendo una petición a un servicio con useEffect(effectFn, [deps])

En un proyecto nuevo de codesandbox.io, crea el archivo OldNewsPapers.js. Lo que se busca con este componente es hacer peticiones a un servicio, donde podemos buscar información sobre periódicos antiguos de USA.

En resumen, la funcionalidad de este ejemplo es que en el campo de búsqueda el usuario escriba algún texto relacionado, cuando el usuario se detenga de escribir por un tiempo mayor al definido, entonces es cuando se dispara la petición.

Primero vamos a hacer solo la petición sin preocuparnos del tiempo definido de espera para ejectuarla.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [newsPaperNumber, setNewsPaperNumber] = useState(0);
  useEffect(() => {
    if (query) {
      console.log("fetch -> ", query);
      fetchOldNewsPapers();
    }
    async function fetchOldNewsPapers() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperNumber(json.totalItems);
    }
  }, [query]);
  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
      </form>
      <section>{newsPaperNumber}</section>
    </>
  );
}

Aquí está el ejemplo en codesandbox.io. Cada vez que query es modificado, se realiza una petición, como se ve en la imagen de abajo.

useEffect usando fetch
useEffect usando fetch

Al instante que el usuario empieza a escribir “texas”, se actualiza el estado de query provocando un “repintado” y se ejecuta la función effecFn por cada letra que se inserte.

Desafortunadamente hacer peticiones a un servicio cada vez que escribimos una letra no es un buen funcionamiento, es mejor solo hacer la petición cuando el usuario deja de escribir, tal como se planteó al principio, asi que vamos a implementar esta mejor solución en la siguiente sección.

useEffect(() => { return function limpieza(){}; }, [deps])

Comentamos previamente que esta función de limpieza se ejecuta solamente en los “repintados”, en el primer pintado no se ejecuta porque la función limpieza es el valor de retorno de la función effectFn. En el pintado número uno la función limpieza aún no existe.

Ejemplo

Para hacer la petición al servicio cuando el usuario deje de escribir, vamos a utilizar un temporizador, este temporizador estará creándose y limpiándose después de cada repintado. Cuando el usuario se detenga de escribir y pase el tiempo definido en el temporizador, ahí es donde haremos la petición al servicio.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [newsPaperCount, setNewsPaperCount] = useState(0);
  const [isTyping, setIsTyping] = useState(false);
  useEffect(() => {
    console.log("función effectFn");
    if (query && !isTyping) {
      console.log("  fetch-->", query);
      fetchOldNewsPaperTitles();
    }
    async function fetchOldNewsPaperTitles() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperCount(json.totalItems);
    }
    let timeoutId;
    if (isTyping) {
      timeoutId = setTimeout(() => {
        console.log('  setIsTyping(false), provoca otro "re-pintado"');
        setIsTyping(false);
      }, 1000);
      console.log("  timeout", timeoutId);
    }
    return () => {
      console.log("----------re-pintado--------------");
      console.log("función limpieza");
      if (timeoutId) {
        clearTimeout(timeoutId);
        console.log("  clearTimeout", timeoutId);
      }
    };
  }, [query, isTyping]);
  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => {
            setIsTyping(true);
            setQuery(e.target.value);
            console.log(
              `onChange: setIsTyping(true) y setQuery(${e.target.value})`
            );
          }}
        />
      </form>
      <section>{newsPaperCount}</section>
    </>
  );
}

Al código de arriba le dejé varios console.log para que podamos ver el flujo, si quieres se los puedes eliminar para mayor claridad. Aquí esta este ejemplo completo en codesandbox.io.

useEffect con limpieza
useEffect con limpieza

Explicación de flujo

Después del primer “pintado” se ejecuta effectFn, luego cuando el usuario empieza a escribir “texas” provoca un “repintado”, lo anterior sucede por cada letra insertada debido al evento onChange que actualiza el estado de query.

Posterior a cada repintado se ejecuta primero la función limpieza, eliminando el temporizador, luego la función effectFn volviendo a activar un temporizador y retornando una nueva función limpieza.

Cuando el usuario deja de escribir, la función callback del setTimeout se ejecuta, provocando la actualización del estado isTyping a false. Lo anterior causa otro repintado adicional, teniendo a isTyping igual a false y query igual a “texas”, siendo las condiciones (query && !isTyping) necesarias para realizar una petición. En el momento que el resultado de la petición está listo, se actualiza setNewsPaperCount con el número de periódicos antiguos que contiene la palabra “texas”. La actualización de setNewsPaperCount provoca un último repintado, solo que en este último repintado no se ejecuta la función limpieza ni effectFn porque query y isTyping, las dependencias de nuestro useEffect, permanecen sin cambiar.

React Hooks personalizados

Las funciones son la unida esencial para la organización y reutilización de código, ya sea solo para separar las responsabilidades de cada función y/o para encapsular código/funcionalidad reutilizable. Los React Hooks perzonalizados, al ser funciones también tienen esta habilidad de organización y reutilización.

Vamos a extraer funciones de nuestro ejemplo anterior con la finalidad de hacer más fácil de leer el código, lo primero que podemos ver es que dentro del useEffect, tenemos un código que realiza una petición usando el API de fetch y otro para llevar el control del temporizador. Tal vez valga la pena separarlos, a estas funciones separadas se les llama React hooks personalizados porque la separación se realiza en forma de Hooks

React Hook personalizado useCanContinueTimeout(trigger, waitTime)

Empezamos con el código del temporizador. Aquí está el código extraído, bueno, con algunas modificaciones para poder ser reutilizado.

function useCanContinueTimeout(trigger, waitTime) {
  const [canContinue, setCanContinue] = useState(false);
  useEffect(() => {
    let timeoutId;
    if (canContinue) {
      timeoutId = setTimeout(() => {
        setCanContinue(false);
      }, waitTime);
    }
    return () => {
      clearTimeout(timeoutId);
    }
  }, [trigger, canContinue]);
  return [canContinue, setCanContinue];
}

Lo que hacemos en realidad es pasar tal cual el código que queremos separar, y luego cambiar los nombres de los identificadores para mejorar su lectura y entendimiento.

Explicación del Reack hook personalizado useCanContinueTimeout

Este React Hook personalizado, puede ser utilizando en cualquier campo de búsqueda donde se quiera realizar una acción después de que el usuario deja de escribir por un tiempo determinado. Recibe dos parámetros, un valor que es el trigger o disparador principal del efecto. Y otro valor que indica el tiempo del temporizador (waitTime), si no se limpia y se crea otro temporizador, canContinue es actualizado a false indicando que la acción de escribir dejo de suceder durante el tiempo indicado por waitTime.

El estado interno canContinue condiciona si puede seguir creando temporizadores o no, si es false ya no puede crear temporizadores, porque la acción de escribir dejo de suceder y si es true indica que el usuario está escribiendo. Se expone este estado canContinue al exterior regresando un par de valores tal cual lo hace useState para su manejo.

La razón por la que se expone al exterior canContinue es porque es más flexible que desde donde se use este hook se pueda definir inmediatamente en que momento puede seguir creando temporizadores o no.

En el caso particular del ejemplo solo le interesa indicarle que si puede continuar creando temporizadores mientras el usuario siga escribiendo dentro del campo de búsqueda.

¿Cómo se puede usar el react hook personalizado useCanContinueTimeout?

Este Hooks se puede usar de la siguiente forma con el ejemplo que hemos estado trabajando.

const WAIT_TIME = 1000;
export default function OldNewsPapers() {
  const [query, setQuery] = useState('');
  const [isTyping, setIsTyping] = useCanContinueTimeout(query, WAIT_TIME);
  ...
  return (...);
}

Aunque se creó pensando en el uso del campo de búsqueda, queda claro que pude ser usado para saber si se detuvo o continua cualquier otra acción. Puede usarse en cargadores, en notificaciones temporales, acciones de scroll en el navegador web y de momento no se me ocurre otro caso, es una primera versión del hook, pero es un ejemplo de como podemos extraer funcionalidad con react hooks personalizados.

React Hook personalizado useGetJsonWhenCondition(url, condition)

Ahora es el turno de la función que realiza la petición a un servicio, primero separamos la funcionalidad, al igual que con useCanContinueTimeout solo copiamos el código y cambiamos los nombres de las dependencias de useEffect.

function useFetchOldNewsPaper(query, condition) {
  const [newsPaperCount, setNewsPaperCount] = useState(0);
  useEffect(() => {
    if (query && !condition) {
      doFetch()
    }
    async function doFetch() {
      const response = await fetch(
        `https://chroniclingamerica.loc.gov/search/pages/results/?andtext=${query}&format=json`
      );
      const json = await response.json();
      setNewsPaperCount(json.totalItems);
    }
  }, [query, condition]);
  return newsPaperCount;
}

React hook personalizado useGetJsonWhenCondition, más reutilizable

Ahora vamos a refactorizarlo para hacerlo más reutilizable.

function useGetJsonWhenCondition(url, condition) {
  const [state, setState] = useState({});
  useEffect(() => {
    if (url && condition) {
      (async () => {
        const response = await fetch(url);
        const json = await response.json();
        setState(json);
      })();
    }
  }, [url, condition]);
  return state;
}

De entrada le cambiamos el nombre para que tenga más sentido sobre lo que hace realmente, obtener un json cuando una condición se cumple. Luego ya no obtendremos solo el número de resultados, mejor ahora guardamos la respuesta completa. Además para no crear una función separada abajo de la condición, se utilizó una función autoinvocada (function() {})().

Solo se hará la petición si se tiene una url y la condición se cumple, el manejo de estos dos datos no es responsabilidad de este hook, así que se la delegamos al usuario final, que en nuestro caso es el componente OldNewsPapers.

Funcion async/await del useEffect

Creamos una función anónima para poder hacer la petición asincrónicamente sin hacer a effectFn asíncrona, ¿Por qué no queremos hacer la función effectFn asíncrona? La respuesta en realidad es simple, normalmente la función effectFn regresa undefined o una función normal de limpieza, pero si la hacemos asíncrona deberá regresar una promesa, lo cual romperá con el funcionamiento normal de valores de retorno de useEffect. Por no mencionar que función effectFn es invocada de manera normal, no de forma async/await.

Aclaramos que la forma anterior donde primero se definía la función por separado de la invocación dentro del effectFn es equivalente a esta función anónima autoinvocada. Al final regresamos el estado del hook, que es la respuesta del servicio o un objeto vacío cuando la respuesta aún no se ha obtenido.

Extraer en su propia función a fetch con petición GET para respuestas JSON.

Finalmente vamos a extraer la funcionalidad de hacer la peticion para que sea mas simple de leer el efecto

function useGetJsonWhenCondition(url, condition) {
  const [state, setState] = useState({});
  useEffect(() => {
    if (url && condition) {
      (async () => {
        setState(await getJson(url));
      })();
    }
  }, [url, condition]);
  return state;
}
// Es simple , sin control de errores para no perder el enfoque de los hooks
async function getJson(url) {
  const response = await fetch(url)
  return await response.json();
}

Separamos el uso de fetch en su propia función getJson, porque el uso de fetch y la llamada al servicio involucra un manejo más detallado de errores, de momento lo dejamos de manera simple separando la invocación.

Implementamos el react hook personalizado useGetJsonWhenCondition en nuestro componente OldNewsPapers

Ahora si se puede utilizar el hook en nuestro componente de ejemplo, de la siguiente forma.

export default function OldNewsPapers({ url }) {
  const [query, setQuery] = useState('');
  const [fetchUrl, setFetchUrl] = useState('');
  const [isTyping, setIsTyping] = useCanContinueTimeout(query, WAIT_TIME);
  const newsPapers = useGetJsonWhenCondition(fetchUrl, !isTyping);
  // Dejamos el manejo de url y la actualización de canCantinue al componente que los utiliza
  useEffect(() => {
    if (query) {
      setIsTyping(true);
      setFetchUrl(url.replace('{query}', query));
    } else {
      setFetchUrl('');
    }
  }, [query, url]);
  return (
    <>
      <h1>Periodicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => {
            setQuery(e.target.value);
          }}
        />
      </form>
      <section>
        {newsPapers.totalItems}
      </section>
    </>
  );
}

Como vemos el manejo de la condición y la formación de la url se la dejamos el componente que usa el hook, no al hook useGetJsonWhenCondition(url, condition). De esta manera el React Hook personalizado useGetJsonWhenCondition(url, condition) puede ser reutilizado, es decir, el componente OldNewsPapers depende de los dos React Hooks personalizados que acabamos de crear, no al revés.

Aquí puedes revisar el ejemplo completo.

Puntos importantes al usar y crear React Hooks Personalizados

Invocaciones y renderizados

Los componentes funcionales se vuelven a ejecutar cuando algo en el estado cambia, y por consiguiente también se ejecutan las funciones de react hooks personalizados. En los ejemplos que vimos, cada vez que cambia algo en el estado de query, fetchUrl, isTyping y newsPapers, provocara otra invocación del componente función OldNewsPaper, por consiguiente también se invocaran las funciones useCanContinueTimeout y useGetJsonWhenCondition (React hooks personalizados) que creamos y todo esto se traduce a un re-renderizado.

¿Por qué NO queremos hacer la función effectFn asíncrona?

La respuesta en realidad es simple, normalmente la función effectFn regresa undefined o una función normal de limpieza, pero si la hacemos asíncrona, deberá regresar una promesa, lo cual romperá con el funcionamiento normal de valores de retorno de useEffect. Por no mencionar que función effectFn es invocada de manera normal, no de forma async/await.

Reglas de React hooks personalizados

Las mismas dos reglas importantes aplicadas a los React Hooks, son tambien aplicadas a los React Hooks personalizados.

  1. Solo ejecuta React Hooks al nivel superior de una función componente o React Hook personalizado
  2. Solo ejecuta React Hooks dentro de funciones de React

Los nombres de los React hooks personalizados deben empezar con “use"

Esto es una convención importante, de esta manera cualquier desarrollador a simple vista se da cuenta que se trata de un React Hook. Ademas asi se nombran los Hooks de React y asi el plugin de eslint detecta posibles errores en el codigo relacionados con las reglas de los hooks.

Cada React hook personalizado, tiene su propio estado

Cada invocación de un React Hook personalizado contiene su propio estado, los Hooks no tienen estados compartidos, porque recordemos que useState() guarda un estado totalmente separado de cualquier otra invocación. Además cada invocación de una función contiene su propio ámbito.

Conclusiones

En esta publicación aprendimos como usar la variante más compleja de useEffect(() => { return function limpieza(){}; }, [deps]) y además se extrajo dos React Hooks personalizados porque el useEffect inicial es complejo. Se estableció que la creación de React Hooks personalizados es igual que cuando extraemos funciones más pequeñas de una función grande. Por último vimos los puntos a tomar en cuenta cuando creamos y usamos React Hooks personalizados.

Referencias

https://reactjs.org/docs/hooks-intro.html

https://reactjs.org/docs/hooks-state.html

https://reactjs.org/docs/hooks-effect.html

https://reactjs.org/docs/hooks-custom.html

React Hooks, useState y useEffect

React Hooks, useState y useEffect

¿Qué son los React Hooks?

Los React Hooks, son funciones que se ejecutan en un determinado punto en la vida de un componente funcional. Permiten usar características de React, sin la necesidad de usar clases. Por ejemplo te permite agregar state a un componente funcional.

También permite controlar efectos colaterales en caso de ser necesarios. Por ejemplo, peticiones a servicios, subscribirse a eventos, modificar el DOM, logging o cualquier otro código imperativo.

¿Por qué funciones y componentes funcionales?

Si necesitas más detalles sobre como son las funciones en Javascript, puedes revisar estas dos publicaciones:

  1. Funciones en Node.js y JavaScript. Lo realmente importante
  2. Funciones en Node.js y Javascript. Más detalles

Organización y reutilización

Se sabe que las funciones son la unida esencial para la organización y reutilización de código, es decir, las funcionalidades de cualquier software.

Al utilizar funciones se elimina la complejidad de usar clases. No necesitas usar this, constructores, ni separar funcionalidades estrechamente relacionadas en varios métodos del ciclo de vida de un componente en React.

Si has creado componentes en React, de seguro has aprendido ver a los componentes como una función y sus propiedades como sus parámetros. ¿Por qué no hacerlo más transparentes al usar solamente funciones?

De hecho las clases, surgieron de funciones. Como se explica aquí y aquí. No necesitamos clases y nos ahorramos muchos dolores de cabeza.

Composición

Es mucho más simple y flexible utilizar funciones en lugar de clases para la composición, tanto de componentes como de cualquier funcionalidad en general.

Mejor composición de objetos sobre herencia de clases

Design Patterns: Elements of Reusable Object-Oriented Software

Simplicidad

Normalmente con las clases, el flujo del código de los efectos colaterales necesita brincar de un método del ciclo de vida a otro, con React Hooks esto es más lineal y fácil de leer. También la definición del estado de un componente es mucho más simple, sin necesidad de definirlo en el constructor.  En el siguiente punto viene un ejemplo de como los React Hooks son más simples.

Se elimina el uso de componentes de alto nivel

Aunque los componentes de alto nivel (HOC) se basan en las funciones de alto nivel, la naturaleza del código HTML y clases hace que esto sea complejo. El código se vuelve difícil de leer y también provoca que los componentes envolventes se aniden demasiado.

Los React Hooks resuelven el famoso “HOC hell”, muy parecido a como las promesas y funciones asíncronas resuelven el “Callback hell”.

HOCs, complejo
HOCs, complejo

Ahora, si se utilizan React Hooks, esos HOC complejos se convierte en un código más simple y lineal.

React hooks, simple y lineal
React hooks, simple y lineal

Más rápido

Es un hecho que las funciones tienen mayor rendimiento que las clases. En el caso de React, un componente de función es más rápido que un componente de alto nivel. Además, usando los React hooks adecuadamente, los componentes con React hooks suelen ser más rápidos que los componentes de clases en React.

Fácil de probar

Haciendo tus pruebas correctas evitando detalles de implementación, tu react hook debería estar cubierto con tus pruebas del componente funcional. En otras publicaciones veremos cómo hacer esto. En caso de que tengas un React Hook complejo, existen herramientas que te facilitan estas pruebas aisladas o también podemos hacer a mano nuestro wrapper que use nuestro React Hook para cada uno de sus casos.

Las dos reglas importantes de React Hooks

Solo ejecuta React Hooks en el nivel superior de la función

La clave de esta regla es que React depende del orden en que se ejecutan los React Hooks.

No ejecutes React Hooks dentro de condiciones, ciclos o funciones anidadas porque se necesita asegurar el orden correcto de los hooks cada vez que el componente se renderiza. Esto permite que React controle correctamente el estado entre multiples useState y useEffect.

Si existe alguna condición en el código, y permite que a veces un hook se ejecute y otras no, el orden se pierde al igual que los datos correctos del estado. Hay que aclarar que las condiciones, ciclos y funciones anidadas dentro de los hooks como en el caso de useEffect si son posibles.

// No hagos esto!!
if (nombre) {
  useEffect(() => {
    document.title = `Algo con ${nombre}`;
  }, [nombre]);
}
// Esto si lo puedes hacer
useEffect(() => {
  if (nombre !== '') {
    document.title = `Algo con ${nombre}`;
  }
}, [nombre]);

Solo ejecuta React Hooks dentro de funciones de React

  1. Ejecuta React Hooks dentro de componentes de función.
    1. En las clases los React Hooks no funcionan, además en clases ya existen los métodos del ciclo de vida de un componente en React.
  2. Ejecuta React Hooks dentro de otros React Hooks (puedes hacer tus propios hooks).
    1. Para garantizar el orden y controlar correctamente el estado entre múltiples Hooks. Te permite ejecutar react hooks al inicio de otro hook creado por ti.

Reac Hooks, useState(initialState)

Empecemos con el hook más utilizado, useState. Este hook proporciona la misma capacidad de los componentes de clases para tener un estado interno. Vamos a hacer un formulario de registro de un usuario nuevo, algo sencillo y genérico, nada de detalles de implementación específicos. Probablemente con el campo nombre será suficiente.

Vamos a utilizar la herramienta de codesandbox.io, usando la plantilla de React. Agregamos un archivo RegistroDeUsuario.js

import { useState } from "react";
export default function RegistroDeUsuario() {
  const [nombre, setNombre] = useState("");
  return (
    <form>
      <label htmlFor="nombre">Nombre</label>
      <input
        type="text"
        value={nombre}
        id="nombre"
        onChange={(e) => setNombre(e.target.value)}
      />
      <button type="submit">Enviar</button>
      <section>{nombre}</section>
    </form>
  );
}

Y en el archivo App.js importamos el componente RegistroDeUsuario

import RegistroDeUsuario from "./RegistroDeUsuario";
export default function App() {
  return (
    <div className="App">
      <RegistroDeUsuario />
    </div>
  );
}

Desctructuring assignment

Primero, la función useState(initialState), regresa un arreglo de dos elementos, el valor del estado (inicializado) que queremos controlar y un método para modificar ese estado. Este par de valores se asignaron a las constantes nombre y setNombre con la sintaxis de destructuring assignment.

El desctructuring assignment de la línea 4 se traduce a lo siguiente.

const statePair = useState('');
const nombre = statePair[0];
const setNombre = statePair[1];

Invocaciones y renderizados

Cada vez que se escribe sobre el campo Nombre, se vuelve a renderizar el componente. Pero React guarda el estado entre renderizados. Se puede notar como la línea 5 se ejecuta al primer rénder (Montar, con texto vacío) y también cada vez que se actualiza el valor de nombre (Actualizar, revisa la consola de la parte derecha de la imagen de abajo).

El resultado debe ser algo como lo siguiente.

Renderizados por useState input Nombre
Renderizados por useState input Nombre

El flujo de useState es el siguiente. Modificado de este original.

Flujo useState
Flujo useState

El valor de nombre se actualiza a través del evento onChange y el método setNombre. Al modificar este estado interno, provoca la ejecución de la función RegistroDeUsuarios y un “re-renderizado”. Si el componente se renderiza debido a un cambio a otro estado u otra propiedad, el estado de nombre permanece con la última actualización.

useState(() => { return initialState; })

useState(initialState) puede recibir una función que regrese el estado inicial usado en el primer render. Un ejemplo de su uso es el que sige, ¿Qué podemos hacer si queremos guardar y obtener el estado de localStorage?

import { useState } from "react";
export default function RegistroDeUsuario() {
  const [nombre, setNombre] = useState(() => {
    console.log("Solo una vez");
    return localStorage.getItem("nombre") || "";
  });
  console.log("Más invocaciones");
  function actualizarNombre(e) {
    setNombre(e.target.value);
    localStorage.setItem("nombre", e.target.value);
  }
  return (
    <form>
      <label htmlFor="nombre">Nombre</label>
      <input
        type="text"
        value={nombre}
        id="nombre"
        onChange={actualizarNombre}
      />
      <button type="submit">Enviar</button>
      <section>{nombre}</section>
    </form>
  );
}

Ahora usamos una función para definir el estado, le agregamos un console.log('Solo una vez') para demostrar que la función solo se ejecuta una vez. Y un console.log('Más invocaciones') para demostrar que en los siguientes invocaciones ya no se ejecuta la función de nuestro useState(initialState), pero si el de Más invocaciones.

En el resultado de abajo, escribí Jaime en el campo nombre, luego recargue la página, revisa el lado derecho en la vista y la consola.

useState, campo nombre en localStorage
useState, campo nombre en localStorage

Al recargar se imprime Sola una vez y al empezar a escribir Cervantes se imprime Más invocaciones un total de 11 veces. Mi nombre jaime, lo obtuvo del localStorage al primer renderizado.

setState(prevState => {})

El método para actualizar el estado setNombre también puede recibir una función, cuyo parámetro es el valor anterior. Veamos un ejemplo modificando la función actualizarNombre.

function actualizarNombre(e) {
  setNombre(nombreAnterior => {
    console.log(nombreAnterior); // '', 'J', 'Ja', 'Jai', 'Jaim'
    return e.target.value;
  });
  localStorage.setItem("nombre", e.target.value);
}

La función setNombre obtenida de useState recibe como parámetro el nombreAnterior, y al imprimir en la consola nos damos cuenta de que siempre imprimirá el valor anterior del estado nombre.

Actualizar state pasando una función
Actualizar el estado pasando una función

useEffect(effectFn, [deps])

Este React hook, useEffect, nos permite ejecutar y controlar efectos colaterales, como pueden ser peticiones a servicios, subscribirse a eventos, modificar el DOM o cualquier funcionalidad que no pueda ejecutarse en el cuerpo de nuestro componente función porque no pertenece al flujo lineal del mismo.

Flujo de useEffect

La función effectFn se ejecuta después de que el navegador ya ha pintado el componente en pantalla por primera vez (montar). También por defecto después de cada posterior repintado (actualizar). Este comportamiento descrito tiene el mismo propósito que los métodos componentDidMount y componentDidUpdate.

El tercer propósito en useEffect se le llama limpieza, el cual lo podemos comparar con componentDidUnmount. En el primer pintado (montar) la función de limpieza no se ejecuta, solo se ejecuta en la fase de actualizar. Es decir, se ejecuta después de cada repintando, pero antes del que el cuerpo de useEffect se ejecute. Este caso en específico se explica mejor con ejemplos en esta publicación.

El flujo de useEffect es el siguiente. Modificado de este original.

Flujo useEffect
Flujo useEffect

useEffect recibe un segundo parámetro, deps, el cual es un Array con la lista de dependencias que permiten decidir si ejecutar el efecto colateral o no, después de cada repintado. Sirve bastante para mejorar el rendimiento si no queremos que después de cada repintado se ejecute effectFn.

Si te das cuenta, he usado la palabra pintado en lugar de renderizado. Esto se debe a que efectivamente el efecto se ejecuta después de que los cambios ya estén pintados en el navegador web. El renderizado involucra al Virtual DOM, y React decide en que momento es conveniente actualizar el DOM, pero el pintado sucede un poco después de lo anterior. Aclaremos que aunque se actualice el DOM, el navegador debe calcular estilos y el layout de los elementos para posteriormente realizar el pintado de los pixeles.

useEffect(effectFn)

Veamos un ejemplo, aquí la idea es tener un input de búsqueda y queremos que lo que tengamos en el input se imprima en título de la pestaña de nuestro navegador web. Para poder realizar esto necesitamos un efecto utilizando la API del DOM.

De nuevo, con la herramienta codesandbox.io y su plantilla de react, creamos un nuevo proyecto. Agregamos un archivo llamado OldNewsPapers.js donde vivirá nuestra funcionalidad en forma de un componente funcional.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  useEffect(() => {
    console.log("document.title");
    document.title = `Periodicos viejos ${query}`;
  });
  console.log('Invocación);
  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
      </form>
    </>
  );
}

Este efecto se ejecuta después de la primera vez que se pinta el componente, esto es el primer propósito que se puede comparar con componentDidMount. ¿Cómo es el flujo?

  1. Se ejecuta la función, es decir, el componente funcional.
    1. Lo cual inicializa el estado de query y el efecto colateral.
  2. Se renderiza y se pinta el elemento de react en el navegador con el valor inicial de query = ''.
    1. Texto “Periódicos viejos que contienen” en el <h1>.
  3. Se ejecuta el efecto colateral después del primer pintado.
    1. Texto “Periódicos viejos que” en el título de la pestaña.
useEffect flujo al montar
useEffect flujo al montar

Si escribimos “texas” en el input, ahora el flujo es de la siguiente manera

  1. Cada vez que se introduce una letra en el input (cinco veces más, por las cinco letras de “texas”)
    1. El estado de query cambia debido al setQuery en el onChange, provocando un nuevo pintado (invocación del componente función, renderizado y finalmente pintado).
    2. Después del pintado se actualiza document.title, cambiando el título de la pestaña del navegador web.
useEffect, flujo actualizar document.title
useEffect, flujo actualizar document.title

En la imagen de arriba vemos seis “document.title”, como describimos al principio, por defecto el useEffect se invoca después de cada pintado en el navegador web.

Puedes ver el código completo en aquí.

useEffect(effectFn, [deps])

En el último ejemplo nuestro efecto se va a ejecutar después de cada pintado, incluso si el estado de query no ha cambiado. Para comprobar esto vamos a agregar otro estado para llevar la cuenta del número de invocaciones de nuestro componente funcional, que se traduce en el número de renderizados realizados. Estas invocaciones las haremos a través de un botón.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);
  useEffect(() => {
    console.log("document.title");
    document.title = `Periódicos viejos ${query}`;
  });
  return (
    <>
      <h1>Periódicos viejos que contienen {query}</h1>
      <p>{invocations} Invocaciones</p>
      <form>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <button
          onClick={(e) => {
            e.preventDefault();
            setInvocations((prev) => prev + 1);
          }}
        >
          Invocar
        </button>
      </form>
    </>
  );
}

Inicializamos invocations con 1, porque la primera vez que se renderiza será el estado actual. Luego si oprimimos el botón Invocar, se cambia el valor de invocations, se hace otro re-renderizado, y luego se vuelve a ejecutar nuestro efecto, incluso cuando query no ha cambiado.

useEffect por defecto siempre se ejecuta después de cada pintado
useEffect por defecto siempre se ejecuta después de cada pintado

Para evitar que se ejecute demasiadas veces nuestro efecto, podemos indicarle que corra solo cuando una de sus dependencias ha cambiado. En este caso para evitar que se le asigne a cada rato el valor a documen.title, le indicamos que solo lo haga cuando query cambia.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);
  useEffect(() => {
    console.log("document.title");
    document.title = `Periódicos viejos ${query}`;
  }, [query]);
  return ( ... );
}

Ahora podemos ver que aunque hicimos muchas invocaciones con el botón “invocar”, document.title solo se ejecutó la primera vez.

useEffect ejecutar solo cuando alguna "dep" cambie
useEffect se ejecuta solo cuando alguna “deps” cambie

useEffect(effectFn, [])

Cuando se especifica un Array vacío en las deps, effectFn solo se ejecuta una sola vez, después del primer pintado.

import { useEffect, useState } from "react";
export default function OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [invocations, setInvocations] = useState(1);
  useEffect(() => {
    console.log("document.title");
    document.title = `Periódicos viejos ${query}`;
  }, []);
  return ( ... );
}

Las siguientes veces que se actualice query escribiendo en el input, el título de la pestaña ya no se actualiza.

useEffect con deps vacío
useEffect con deps vacío

Conclusión sobre

Las funciones han existido desde mucho antes de la programación, gracias al cálculo lambda de Alonzo Church. No es extraño que en los últimos años el desarrollo de software ha volteado hacia la programación funcional debido a la simplicidad y el poder expresivo. Resolviendo varios problemas en el camino.

Y bueno, con el uso de React Hooks, se ha dado un paso muy importante debido a los beneficios que es programar de esta manera, desde hace años que se utilizaban componentes funcionales, y ahora con esto creo que React tiene más futuro prometedor por delante.

Hemos entendido, con suficiente profundidad (para comenzar con hooks), como funcionan los flujos de los Hooks useState y useEffect. De useEffect aún quedan temas por ver, así como también los React Hooks personalizados. Aquí pondré el enlace con los temas pendientes, cuando estos estén publicados. Cualquier duda, no dudes en escribirla en los comentarios, ¡Estaremos contentos de ayudarte!.

Referencias

https://reactjs.org/docs/hooks-intro.html

https://reactjs.org/docs/hooks-state.html

https://reactjs.org/docs/hooks-effect.html

Foto de fondo de portada por Artem Sapegin en Unsplash

es_MXES_MX