Como crear un buscador de criptomonedas con Redux, parte  1

Como crear un buscador de criptomonedas con Redux, parte 1

Introducción

En esta publicación veremos como utilizar redux para obtener datos de una API. Con estos datos obtenidos vamos a llenar una lista de criptomonedas y agregar un buscador.

Para comprender el contenido debes de saber lo que es redux y sus principios. Si quieres más información fundamental sobre como usar redux, puedes revisar estas publicaciones:

¿Qué queremos lograr?

Buscador parte 1 terminada
Buscador parte 1 terminada

En la imagen tenemos un campo de búsqueda y una lista con todos los mercados disponibles. Puedes crear un proyecto de React en codesandbox.io.

Aquí puedes ver como debe funcionar el buscador en esta primera parte.

https://b8lxc.csb.app/

Primera iteración, datos en duro en los componentes

La idea de empezar con los datos en duro es conocer el formato de lo que responde la API y como esos datos los queremos encajar en los componentes visuales.

¿Cómo son los datos de la respuesta del API?

Primero revisemos la forma de los datos que regresa la API. Estos datos son tomados de aquí. Más adelante simularemos la petición al API para facilitar el desarrollo.

{
  "success": true,
  "payload": [
    {
      "high": "1031000.00",
      "last": "1007500.03",
      "created_at": "2021-12-22T18:51:21+00:00",
      "book": "btc_mxn",
      "volume": "85.76293860",
      "vwap": "1015844.0131800048",
      "low": "1003978.08",
      "ask": "1007766.40",
      "bid": "1007500.03",
      "change_24": "-1808.15"
    },
    ...
  }
}

Vamos a crear la interfaz pensando en los datos mostrados y componentes, de momento se me ocurre una tabla donde estarán listados los detalles del mercado y un campo de búsqueda.

Componente CryptoMarkets

Entonces agregamos un componente contenedor /src/CrytoMarkets/CryptoMarkets.js. En este componente contenedor vivirán los dos componentes antes mencionados, uno para hacer la búsqueda y otro para la tabla donde se listaran los mercados y el resultado de la búsqueda.

export default function CryptoMarkets() {
  return (
    <>
      <SearchField />
      <Table />
    </>
  );
}

Componente SearchField

Creamos el componente SearchField en /src/CryptoMarkets/SearchField.js

export default function SearchField({ label }) {
  return (
    <label>
      {label}
      <input type="search" />
    </label>
  );
}

Componente Table

Y el componente Table en /src/CryptoMarkets/Table.js

import styles from "./Table.module.scss";
export default function Table() {
  return (
    <div>
      <div className={styles.row}>
        <p>Mercado</p>
        <p>Moneda</p>
        <p>Último precio</p>
        <p>Volumen</p>
        <p>Precio más alto</p>
        <p>Precio más bajo</p>
        <p>Variación 24hrs</p>
        <p>Cambio 24hrs</p>
      </div>
      <div className={styles.row}>
        <p>btc/mxn</p>
        <p>btc</p>
        <p>1007500.03</p>
        <p>85.76293860</p>
        <p>1031000.00</p>
        <p>1003978.08</p>
        <p>1015844.01</p>
        <p>-1808.15</p>
      </div>
      <div>
        <p>eth/btc</p>
        <p>eth</p>
        <p>0.08</p>
        <p>52.02866824</p>
        <p>0.08</p>
        <p>0.08</p>
        <p>0.08</p>
        <p>-0.00040000</p></div>
    </div>
  );
}

Y para que se vea como una tabla le agregamos los siguientes estilos en /src/CrytoMarkets/Table.module.scss. Los estilos no es tema de esta publicación así que no le tomes mucha importancia por el momento, en caso de que se te compliquen.

Lo único es que si no estás haciendo el buscador en codesandbox y te sale algún problema con los estilos en sass, instala la librería de la siguiente forma.

yarn add sass
.row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
  border-bottom: 1px solid #eee;
  &:hover {
    background-color: #efefef;
  }
  &:last-child  {
    border-bottom: none;
  }
}
.header:hover {
  background-color: initial;
}
.item {
  word-wrap: break-word;
  padding: 8px;
  &:first-child {
    text-transform: uppercase;
  }
}
.highlight {
  font-weight: bold;
}

En la tabla podemos notar que los divs que contiene los datos y el div del encabezado son muy similares, dentro se encuentran varios párrafos conteniendo cada pedazo de información. Así que crearemos un componente que hago eso, lo que contiene cada fila de la tabla.

Componente TableRow

/src/CryptoMarkets/TableRow.js

export default function TableRow({ items, className}) {
  return (
    <div className={className}>
      {items.map((item, index) =>
        <p key={index} className={item.className}>
          {item.value}
        </p>
      )}
    </div>
  );
}

Usando datos dummies.js

Dentro del componente Table agregamos dos arreglos, uno para el header de la tabla y otro para una fila de la tabla. Como mencionamos antes, el header y las filas son muy similares, así que reutilizamos el mismo componente TableRow para generarlos.

import styles from "./Table.module.scss";
import TableRow from "./TableRow";
const headers = [
  { value: "MERCADO", className: `${styles.item} ${styles.highlight}` },
  { value: "Moneda", className: `${styles.item} ${styles.highlight}` },
  { value: "Último precio", className: `${styles.item} ${styles.highlight}` },
  { value: "Volumen", className: `${styles.item} ${styles.highlight}` },
  { value: "Precio más alto", className: `${styles.item} ${styles.highlight}` },
  { value: "Precio más bajo", className: `${styles.item} ${styles.highlight}` },
  { value: "Variación 24hrs", className: `${styles.item} ${styles.highlight}` },
  { value: "Cambio 24hrs", className: `${styles.item} ${styles.highlight}` }
];
const row = [
  { value: "BTC/MXN", className: styles.item },
  { value: "btc", className: styles.item },
  { value: "1007500.03", className: styles.item },
  { value: "85.76293860", className: styles.item },
  { value: "1031000.00", className: styles.item },
  { value: "1003978.08", className: styles.item },
  { value: "1015844.01", className: styles.item },
  { value: "-1808.15", className: styles.item }
];
export default function Table() {
  return (
    <div>
      <TableRow items={headers} className={`${styles.row} ${styles.header}`} />
      <TableRow items={row} className={styles.row} />
    </div>
  );
}

Como te puedes dar cuenta el componente TableRow recibe items y className como propiedades, para que el header de la table no tenga el efecto del estilo :hover agregamos la clase .header.

Generando filas dinámicamente y dummies separados.

Primero separamos los dummies y les damos forma.

import styles from "./Table.module.scss";
export const headers = [
  { value: "MERCADO", className: `${styles.item} ${styles.highlight}` },
  { value: "Moneda", className: `${styles.item} ${styles.highlight}` },
  { value: "Último precio", className: `${styles.item} ${styles.highlight}` },
  { value: "Volumen", className: `${styles.item} ${styles.highlight}` },
  { value: "Precio más alto", className: `${styles.item} ${styles.highlight}` },
  { value: "Precio más bajo", className: `${styles.item} ${styles.highlight}` },
  { value: "Variación 24hrs", className: `${styles.item} ${styles.highlight}` },
  { value: "Cambio 24hrs", className: `${styles.item} ${styles.highlight}` }
];
export const rows = [
  {
    id: "btc/mxn",
    className: styles.row,
    items: [
      { value: "btc/mxn", className: styles.item },
      { value: "btc", className: styles.item },
      { value: "1007500.03", className: styles.item },
      { value: "85.76293860", className: styles.item },
      { value: "1031000.00", className: styles.item },
      { value: "1003978.08", className: styles.item },
      { value: "1015844.01", className: styles.item },
      { value: "-1808.15", className: styles.item }
    ]
  },
  {
    id: 2,
    className: styles.row,
    items: [
      { value: "eth/btc", className: styles.item },
      { value: "eth", className: styles.item },
      { value: "0.08", className: styles.item },
      { value: "52.02866824", className: styles.item },
      { value: "0.08", className: styles.item },
      { value: "0.08", className: styles.item },
      { value: "0.08", className: styles.item },
      { value: "-0.00040000", className: styles.item }
    ]
  }
];

Luego utilizamos esos dummies en instancias del componente TableRow.

import styles from "./Table.module.scss";
import TableRow from "./TableRow";
import { headers, rows } from "./dummies";
export default function Table() {
  return (
    <div>
      <TableRow items={headers} className={`${styles.row} ${styles.header}`} />
      {rows.map((row) => (
        <TableRow key={row.id} items={row.items} className={row.className} />
      ))}
    </div>
  );
}

Usamos el nombre del mercado como el id porque es un dato que no se puede repetir. En este punto debemos tener algo así:

Buscador con dummies
Buscador con dummies

Segunda Iteración, agregando Redux

Para usar redux, necesitamos instalar la librería redux y para facilitar su uso en React instalamos también react-redux. Desde condesandbox puedes agregar estas dependencias en la parte inferior izquierda del editor donde dice “Dependencies“.

En tu computadora local corre el siguiente comando.

yarn add redux react-redux

En este punto ya sabemos la forma en que necesitamos los datos, la tabla necesita unos headers y una lista de filas con valores (rows).

Además, necesitamos crear un store que sea accesible desde cualquier parte de nuestra aplicación. El principal insumo que necesita el store es un reducer. Aún no obtendremos los datos de los mercados de la API, utilizaremos de momento los datos del archivo /src/CryptoMarkets/dummies.js

Configurando inicialmente el store

Crearemos un archivo nuevo /configureStore.js.

import { createStore } from "redux";
import { headers, rows as markets } from "./CryptoMarkets/dummies";
const initialState = {
  headers,
  markets
};
export default function configureStore() {
  return createStore((state, action) => state, initialState);
}

Importamos la función createStore que recibe una función reducer y opcionalmente un estado inicial. La función reducer de momento solo regresará el estado inicial tal cual, como sabemos, en redux las funciones reducers reciben un estado y una acción. Como segundo parámetro opcional le pasamos los datos dummies.

La función configureStore la utilizaremos a continuación.

Hacer al store accesible

Para que cualquier componente pueda acceder al store es necesario crear un contexto global, esto normalmente se realiza con React.createContext() y usando su componente Context.Provider. Por suerte la librería react-redux ya cuenta con este funcionamiento y podemos utilizar su propio componente Provider.

Si tienes curiosidad como usar el Context de React, puedes revisar esta publicación:

useContext y useReducer, ¿Cómo replicar redux?

Aplicamos el Provider de react-redux en nuestra aplicación de la siguiente forma. En nuestro archivo /index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import configureStore from "./configureStore";
const store = configureStore();
const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>,
  rootElement
);

Al componente Provider debemos pasarle una propiedad store. Esa propiedad es el store generado por la función configureStore que creamos en la sección anterior.

Acceder al store desde el componente contenedor CryptoMarkets

Ahora que ya tenemos el store listo para usarse, vamos a obtener el estado para mostrar el listado de markets en el componente Table.

import Table from "./Table";
import SearchField from "./SearchField";
import { useSelector } from "react-redux";
export default function CryptoMarkets() {
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.markets);
  return (
    <>
      <SearchField></SearchField>
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Usamos lo que en redux llaman selectors, los cuales son funciones que acceden a una parte especifica del estado para facilitar su acceso. Se usa el “custom hook” useSelector de react-redux para invocar las funciones selectoras.

Componente Table recibe headers y rows como propiedades

Finalmente, el componente Table debe poder recibir las propiedades headers y rows en lugar de importarlos del dummies.js

import styles from "./Table.module.scss";
import TableRow from "./TableRow";
export default function Table({ rows, headers }) {
  return (
    <div>
      <TableRow items={headers} className={`${styles.row} ${styles.header}`} />
      {rows.map((row) => (
        <TableRow key={row.id} items={row.items} className={row.className} />
      ))}
    </div>
  );
}

El resultado deberá ser el mismo de la primera iteración, pero con los datos obtenidos del store

Buscador con dummies
Buscador con dummies

Implementar la busqueda de mercados

La búsqueda que vamos a implementar será por el nombre del mercado. Así que basándonos en los datos del archivo /src/CrytoMarkets/dummies.js, vamos a modificar nuestra función reducer para agregar la acción de filtrado.

Creamos el archivo /src/CryptyoMarkets/cryptoMarketsState.js donde ahora vivirá nuestro reducer, separándolo del archivo /configureStore.js.

// state = { headers [], all = [], filtered = []}
export default function cryptoMarkets(state, action) {
  switch (action.type) {
    case "FILTER_MARKETS":
      return {
        ...state,
        filtered: state.all.filter(
          (market) => market.id.includes(action.payload)
        )
      };
    default:
      return state;
  }
}

El comentario muestra la estructura del estado. La estructura sé cambio porque necesitamos guardar la lista original de mercados para ser utilizado en cada búsqueda, de lo contrario filtraríamos mercados sobre los últimos filtrados y así sucesivamente eliminando todos los mercados.

Una acción es un objeto literal que contiene un tipo y demás datos necesarios para cambiar el estado del store, estos datos los vamos a guardar en una propiedad llamada payload.

La acción 'FILTERED_MARKETS', filtra todos los markets que en su propiedad id incluyan el texto contenido en payload.

 state.all.filter(
   (market) => market.id.includes(action.payload)
 )

Luego en /configureStore.js reflejamos esa nueva estructura en el initialState e importamos nuestra nueva función reducer.

import { createStore } from "redux";
import { headers, rows as markets } from "./CryptoMarkets/dummies";
import reducer from "./CryptoMarkets/cryptoMarketsState";
const initialState = {
  headers,
  all: markets,
  filtered: markets
};
export default function configureStore() {
  return createStore(reducer, initialState);
}

Disparar acción de filtrado desde el componente CryptoMarkets

Aunque ya creamos la acción, debemos dispararla cuando el usuario escribe en el campo de búsqueda, primero vamos a modificar el componente SearchField. Ahora además de recibir el label como propiedad, también la función que se ejecuta cada vez que el valor de búsqueda cambia.

export default function SearchField({ label, onSearchChange }) {
  return (
    <label>
      {label}
      <input type="search" onChange={onSearchChange} />
    </label>
  );
}

De esta manera dentro del componente CryptoMarkets podemos disparar la acción FILTER_MARKETS como sigue.

import Table from "./Table";
import SearchField from "./SearchField";
import { useSelector, useDispatch } from "react-redux";
export default function CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  function onSearchMarket(e) {
    dispatch({ type: "FILTER_MARKETS", payload: e.target.value });
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Se usa el “custom hook” useDispatch de react-redux para disparar acciones desde cualquier componente funcional. Este hook regresa una función dispatch que se utiliza para disparar cualquier acción.

Al componente SearchField se le pasa la propiedad onSearchChange donde recibe la función que invoca la acción cuando el valor del campo de búsqueda cambia.

El resultado de esta iteración debe ser algo como lo siguiente:

Buscador implementado
Buscador implementado

Refactor, action creator para disparar búsqueda

Los action creators no son más que simples funciones que regresan una acción, y una acción es simplemente un objeto literal con un tipo y datos necesarios para la acción. En esta ocasión vamos a crear el action creator para la acción FILTER_MARKETS. En el archivo /src/CryptoMarkets/cryptoMarketsState.js agregamos al final lo siguiente

export const searchMarket = (filter) => ({ type: 'FILTER_MARKETS', payload: filter });

Y ahora en nuestro componente /src/CryptoMarkets/CryptoMarkets.js

import Table from "./Table";
import SearchField from "./SearchField";
import { useSelector, useDispatch } from "react-redux";
import { searchMarket } from "./cryptoMarketsState";
export default function CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  function onSearchMarket(e) {
    dispatch(searchMarket());
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Tercera iteración, obteniendo datos de la API simulada

En esta iteración vamos a simular las peticiones del API para dejar de usar los rows del archivo /src/CryptoMarkets/dummies.js.

Respuesta json de bitso

Simplemente en tu navegador pega la siguiente url:

https://bitso.com/api/v3/ticker?book=all

Mercados de bitso
Mercados de bitso

Ahora copia el json que te respondió y pégalo en un nuevo archivo llamado /api/bitso-markets.json.

Luego crea un archivo llamado /api/index.js, ahí vamos a agregar la función que simule la petición.

import markets from "./bitso-markets.json";
export function fetchMarkets() {
  return new Promise((resolve) => {
    setTimeout(() => resolve(markets), 1000);
  });
}

En el código anterior se importa el json en una variable llamada markets y se crea una promesa con un temporizador de un segundo, después del segundo la promesa se resuelve retornando los datos de los mercados (contenidos en la variable markets).

Usar redux-thunk para obtener los markets

¿Qué es un thunk?

Su definición en el ámbito de programación en general:

Los thunks se utilizan principalmente para retrasar un cálculo hasta que se necesite su resultado.

https://en.wikipedia.org/wiki/Thunk

En react se utiliza para escribir funciones en redux con lógica avanzada relacionadas con el estado de la aplicación. Donde se puede trabajar con operaciones tanto asíncronas como síncronas. Estas funciones thunk tienen acceso a las funciones dispatch() y getState() de redux, por esta razón un thunk internamente puede disparar cualquier numera de acciones y obtener los datos actuales del estado por si son necesarios en su lógica de programación.

Crear un Thunk action creator

En realidad un thunk se usa como se usan los action creators, por ese se les llama Thunk action creator.

// actions
export function getMarkets() {
  return async (dispatch) => {
    const rawMarkets = await fetchMarkets();
    dispatch({ type: "RECEIVE_MARKETS", payload: rawMarkets.payload });
  };
}

En el codigo de arriba, el action creator regresa una función asíncrona que recibe solo la función dispatch (no vamos a necesitar la función getState()). La función comienza pidiendo los markets y una vez obtenidos, dispara la acción RECEIVE_MARKETS.

Actualizar reducer para entender la acción RECEIVE_MARKETS

En el archivo /CryptoMarkets/cryptoMarketsState.js agregamos él case para la acción RECEIVE_MARKETS. El código de este archivo debe quedar como el de abajo.

import { fetchMarkets } from "../api";
export default function cryptoMarkets(state, action) {
  switch (action.type) {
    case "FILTER_MARKETS":
      return {
        ...state,
        filtered: state.all.filter((market) =>
          market.id.includes(action.payload)
        )
      };
    case "RECEIVE_MARKETS":
      return {
        ...state,
        all: action.payload,
        filtered: action.payload,
      };
    default:
      return state;
  }
}
// actions
export const searchMarket = (filter) => ({ type: 'FILTER_MARKETS', payload: filter });
export function getMarkets() {
  return async (dispatch) => {
    const rawMarkets = await fetchMarkets();
    dispatch({ type: "RECEIVE_MARKETS", payload: rawMarkets.payload });
  };
}

Hacer que el store entienda las funciones thunk

Para que el thunk se ejecute correctamente es necesario indicarle al store como tratar este tipo de funciones, para eso necesitamos agregar el middleware de react-thunk. Un middleware extiende o mejora la funcionalidad en un punto medio del software o comunica diferentes puntos de la aplicación, y también puede comunicar aplicaciones diferentes. De hecho en redux también se les llama enhancers.

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from "./CryptoMarkets/cryptoMarketsState";
import { headers } from "./CryptoMarkets/dummies";
const initialState = {
  headers,
  all: [],
  filtered: [],
};
export default function configureStore() {
  return createStore(reducer, initialState, applyMiddleware(thunk));
}

Primero importamos la función applyMiddleware, luego importamos el middleware llamado thunk y finalmente aplicamos el middleware en el terecer parametro de createStore.

También cambiamos el initialState, los header los dejamos en el archivo dummies.js. Las propiedades all y filtered las cambiamos a array vacíos.

Invocando el thunk desde el componente CryptoMarkets

Por último, necesitamos invocar nuestro thunk para obtener los markets de nuestro servicio simulado. El archivo /CrytoMarkets/CryptoMarkets.js donde vive el componente padre CryptoMarkets debe quedar como el de abajo.

import Table from "./Table";
import SearchField from "./SearchField";
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
import { getMarkets, searchMarket } from "./cryptoMarketsState";
export default function CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  useEffect(() => {
    dispatch(getMarkets());
  }, [dispatch]);
  function onSearchMarket(e) {
    dispatch(searchMarket(e.target.value));
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Importamos el thunk getMarkets y ocupamos el hook useEffect. Dentro invocamos a getMarkets() para pedir el listado de markets al api simulado.

useEffect(() => {
    dispatch(getMarkets());
  }, [dispatch]);

En este punto nuestra aplicación nos debe mostrar un error porque el formato de las propiedades de los componentes no son iguales a la respuesta del api, pero podemos agregar un console.log(action) en el reducer para comprobar que el api simulado está respondiendo con los datos.

case "RECEIVE_MARKETS":
  console.log(action);
  return {
    ...state,
    all: action.payload,
    filtered: action.payload
  };

El error debe mostrarse asi:

Error, falta de mapeo
Error falta de mapeo

En la imagen de arriba vemos en la consola que recibimos los datos del API simulado. En la siguiente sección eliminaremos este error mapeando correctamente los datos al formato de como los necesita el componente.

Mapeando datos a UI

Para que el error desaparezca, necesitamos mapear los datos obtenidos del servicio a datos que entienda los componentes, para esto creamos el archivo /mappers.js y vamos a crear las funciones de mapeo:

import styles from "./Table.module.scss";
export function mapMarketsProps(markets) {
  return markets.map((market) => {
    const { book, last, high, low, change_24, vwap, volume } = market;
    const vwapNum = Number(vwap);
    const change24Num = Number(change_24);
    return {
      currency: book.split("_")[0],
      market: book.replace("_", "/"),
      variation24hrs: vwapNum > 0 ? vwapNum.toFixed(2) : vwap,
      change24hrs: change24Num > 0 ? change24Num.toFixed(2) : change_24,
      last: Number(last).toFixed(2),
      high: Number(high).toFixed(2),
      low: Number(low).toFixed(2),
      volume: Number(volume).toFixed(2),
    };
  });
}
export function mapMarketsToUi(rawMarkets) {
  const mappedMarkets = mapMarketsProps(rawMarkets.payload);
  return mappedMarkets.map((ticker) => {
    return {
      id: ticker.market,
      className: styles.row,
      items: [
        { value: ticker.market, className: styles.item },
        { value: ticker.currency, className: styles.item },
        { value: ticker.last, className: styles.item },
        { value: ticker.volume, className: styles.item },
        { value: ticker.high, className: styles.item },
        { value: ticker.low, className: styles.item },
        { value: ticker.variation24hrs, className: styles.item },
        { value: ticker.change24hrs, className: styles.item }
      ]
    };
  });
}

Aquí ya podemos usar esas funciones de mapeos en nuestro componente contenedor:

import Table from "./Table";
import SearchField from "./SearchField";
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
import { getMarkets, searchMarket } from "./cryptoMarketsState";
import { mapMarketsToUi } from "../mapppers";
export default function CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  useEffect(() => {
    dispatch(getMarkets(mapMarketsToUi));
  }, [dispatch]);
  function onSearchMarket(e) {
    dispatch(searchMarket(e.target.value));
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Y modificamos nuestro thunk de la siguiente forma.

export function getMarkets(mapToUi) {
  return async (dispatch) => {
    const rawMarkets = await fetchMarkets();
    const uiMappedMarkets = mapToUi(rawMarkets);
    dispatch({ type: "RECEIVE_MARKETS", payload: uiMappedMarkets });
  };
}

Ahora la función de mapeo es recibida por nuestro thunk, de esta manera, podemos utilizar diferentes mapeos para diferentes tipos de componentes. Imagínate que tenemos otra pantalla donde necesitamos mostrar la misma información en una lista de cards, ¿Verdad que el mapeo es diferente? ´Todo lo demás se deja intacto y solo cambiamos el mapeo.

Para esta pantalla donde tenemos una tabla, el resultado es el siguiente:

Resultado final parte 1
Error arreglado

Estilos en campo de búsqueda

Un toque final, vamos a mejorar el campo de búsqueda creando estos estilos:

.label {
  display: inline-flex;
  flex-direction: column;
  font-weight: bold;
  font-size: 1.2rem;
  margin-bottom: 1rem;
}
.input {
  padding: 8px;
  font-size: 1.2rem;
}

En el componente importamos y aplicamos estilos:

import styles from "./SearchField.module.scss";
export default function SearchField({ label, onSearchChange }) {
  return (
    <label className={styles.label}>
      {label}
      <input type="search" onChange={onSearchChange} className={styles.input} />
    </label>
  );
}

Ahora si tenemos el resultado final:

Buscador parte 1 terminada
Buscador parte 1 terminada

Conclusión. Más iteraciones

Todavía quedan más cosas por hacer y mejorar, al igual que hicimos en esta publicación incrementando en iteraciones pequeñas hasta conseguir que funcione el buscador, así tendremos más publicaciones para continuar mejorando e incrementando aún más las funcionalidades.

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

¿Cómo funciona React con ReactDOM y JSX?

¿Cómo funciona React con ReactDOM y JSX?

En esta publicación veremos como crear un componente, y comprender como se relaciona React con ReactDOM y JSX. Tener buenos fundamentos sobre el funcionamiento de React es muy importante para aprender más rápido esta librería. Además, al entender bien estos fundamentos, es fácil arreglar errores a la hora de crear nuestros componentes y aplicaciones.

Primero utilizaremos solo React para crear un elemento y ReactDOM para pintarlo, luego agregaremos JSX y finalmente veremos como podemos convertir ese elemento en un componente reutilizable.

Para los que no tienen idea que es React, es una librería escrita en JavaScript que facilita la creación de interfaces gráficas de usuario para aplicaciones web y móviles. Esta basada en componentes y composición en lugar de herencia.

Vamos a usar una herramienta en línea llamada condesandbox. Aquí crearemos un nuevo sandbox con la plantilla de HTML 5, tambien se le llama static.

Plantilla sandbox HTML5
Plantilla sandbox HTML5

Luego creamos un archivo api-dom.html y otro react.html, en el body de ambos archivos agregamos un div con el atributo id igual a "raiz-app", en este elemento contenedor vamos a insertar todos los elementos nuevos.

Todo lo referente a API DOM deber estar en el archivo api-dom.html y todo lo referente a React en react.html.

API DOM: Crear un elemento saludo simple

El objetivo es crear un elemento donde saludemos a nuestro usuario a través de un texto. Primero usaremos el API del DOM que se encuentra en todos los navegadores. Esto nos ayudará a comparar y visualizar los beneficios de usar React.

<body>
  <div id="raiz-app"></div>
  <script>
    const saludo = document.createElement('div');
    saludo.textContent = 'Hola, soy un elemento saludo creado con API DOM';
    const raiz = document.getElementById('raiz-app');
    raiz.appendChild(saludo);
  </script>
</body>

Creamos un elemento div y dentro agregamos el texto del saludo. Finalmente agregamos nuestro saludo a nuestro contenedor raiz-app con node.appenChild(node).

Si abres tu archivo api-dom.html usando el navegador web del lado derecho de la interfaz de condesandbox, el resultado debe ser algo así.

Resultado de document.createElement
Resultado de document.createElement

React: crear elemento saludo

Ahora vamos a crear el mismo elemento de la sección anterior con React.

Importar código de la librería

Para empezar a utilizar React es necesario descargar su código, existen varias formas de usar el código de React, por el momento simplemente descargamos la librería usando la etiqueta <script> dentro de la etiqueta <head> de nuestro archivo react.html. De la siguiente manera.

<head>
  <title>React.createElement</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
</head>

Algo muy bueno de React es que separa las responsabilidades, lo que acabamos de agregar, descarga el código necesario para crear elementos y componentes. Para poder pintar esos elementos en nuestra página web, usamos ReactDOM. Pintar elementos en la página web desde luego que debe ser otra responsabilidad.

Para usar ReactDOM.render() necesitamos descargar su código usando de nuevo la etiqueta <script>, igual que hicimos con React.

<head>
  <title>React.createElement</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
</head>

Crear saludo con React.createElement() y ReactDOM.render()

Ahora sí, creamos nuestro nuevo elemento usando React.createElement(component, props, ...children)

<body>
  <div id="raiz-app"></div>
  <script>
    const saludo = React.createElement(
      'div',
      null,
      'Hola, soy un elemento saludo creado con React');
	ReactDOM.render(saludo, document.getElementById('raiz-app'));
  </script>
</body>

La primera diferencia con document.createElement(), es que React.createElement() recibe como parámetros adicionales un objeto de propiedades y luego cualquier número de parámetros que se convertirán en elementos hijos.

De momento no le agregamos propiedades, pero si queremos agregar el saludo como un elemento de tipo texto, este elemento de tipo texto será un elemento hijo. Es lo que en API DOM seria node.textContent.

Finalmente pintamos nuestro saludo usando el método ReactDOM.render(). Por el momento digamos que ReactDOM.render() es como el node.appendChild().

Resultado de React.createElement
Resultado de React.createElement

Podemos agregar más elementos hijos simplemente agregando más parámetros, de la siguiente forma:

const saludo = React.createElement(
  'div',
  null,
  'Hola, soy un saludo creado con React',
  ' Y yo soy otro hijo',
  ' Y yo otro hijo más'
);
ReactDOM.render(saludo, document.getElementById('raiz-app'));

El parámetro children es un parámetro rest, el cual puede tomar cualquier número de parámetros e internamente hacer referencia a ellos a través de un array, React sabe como usarlos para pintarlos. En este ejemplo children contiene lo siguiente.

children = [
  'Hola, soy un saludo creado con React',
  ' Y yo soy otro hijo',
  ' Y yo otro hijo más'
];

API DOM: Modificamos el elemento saludo agregando propiedades y estilos

Nuestro elemento hasta ahora es demasiado simple, vamos a agregarle algunos estilos, y para eso vamos a utilizar dos propiedades, className y style.

const saludo = document.createElement('div');
saludo.textContent = 'Hola, soy un elemento saludo creado con API DOM';
saludo.className = "saludo";
saludo.style = 'font-weight:bold; text-transform:uppercase;';
const contenedor = document.getElementById('raiz-app');
contenedor.appendChild(saludo);

La propiedad className no se llama class para evitar conflictos, debido a que class es una palabra reservada en JavaScript usada para definir clases.

Ahora creamos la reglas de estilo para la clase de estilos saludo. Vamos a usar la etiqueta <style> dentro de <head>.

<head>
  <style>
    .saludo {
      background: orange;
      padding: 16px;
    }
  </style>
<head>

La propiedad style también aplica estilos, en este ejemplo pone el texto en negritas lo convierte a mayúsculas. El resultado debe ser algo como la imagen de abajo.

Resultado de document.createElement con estilos y propiedades
Resultado de document.createElement con estilos y propiedades

React: Modificamos el elemento saludo agregando propiedades y estilos

En props podemos agregar las propiedades que necesitemos, en este ejemplo solo agregamos className y style.

const saludo = React.createElement('div', {
    className: 'saludo',
    style: {
      'font-weight': 'bold',
      'text-transform': 'uppercase'
    },
  },
  'Hola, soy un elemento saludo creado con React'
);
const contenedor = document.getElementById('raiz-app'))
ReactDOM.render(saludo, container);

En la propiedad style, los estilos son agregados a través de un objeto en lugar de una cadena de caracteres, esto es más cómodo y legible, ¿Imagínate que fueran muchos estilos?, la supercadena larga que se formaría.

Tampoco no es que se recomiende esta forma de aplicar estilos a tus elementos en React, pero podemos aprovechar esta comodidad por el momento.

Otro beneficio en la propiedad style, es que en las declaraciones de estilos pueden ser escritas en camelCase, de hecho esa es la convención en Javascript y en React. Es más simple escribir en camelCase que estar encerrando todas las propiedades en comillas y con guiones medios entre palabras.

const saludo = React.createElement('div', {
    style: {
      fontWeight: 'bold',
      textTransform: 'uppercase'
    },
  },
  'Hola, soy un elemento saludo creado con React'
});

Si usas la forma, property-name, React te mostrara unos “warnings” en la consola. Estos “warnings” solo saldrán en modo develop, en producción no hay problema. Solo es una convención y no es obligatoria.

Warning: Unsupported style property font-weight. Did you mean fontWeight?
    at div
printWarning @ react-dom.development.js:61

Por último agregamos los estilos CSS que usara la clase saludo.

<head>
  <style>
    .saludo {
      border: 1px solid #eee;
      padding: 10px;
    }
  </style>
<head>

Al final debes tener un resultado como el de la imagen de abajo.

Resultado de React.createElement y definiendo propiedades
Resultado de React.createElement y definiendo propiedades

React: children en props

El contenido de nuestro saludo lo podemos agregar en el objeto props, en la propiedad children, en lugar del último parámetro de la función. children, en este caso funciona como la propiedad textContent del ejemplo usando API DOM.

const saludo = React.createElement('div', {
  className: 'saludo',
  style: {
    fontWeight: 'bold',
    textTransform': 'uppercase'
  },
  children: 'Hola, soy un elemento saludo creado con React'
});
ReactDOM.render(saludo, document.getElementById('raiz-app'));

En un ejemplo anterior comentamos que el tercer parámetro children es un parámetro rest, el cual recibe cualquier número de parámetros los almacena en un Array. Pues la propiedad children también puede recibir un Array de hijos que React puede pintar.

const saludo = React.createElement('div', {
  className: 'saludo',
  style: {
     fontWeight: 'bold',
     textTransform: 'uppercase'
  },
  chidren: [
    'Hola, soy un saludo creado con React',
    ' Y yo soy otro hijo',
    ' Y yo otro hijo más'
  ]
);
const contenedor = document.getElementById('raiz-app');
ReactDOM.render(saludo, contenedor);

A lo mejor te parezca un poco complicado, pero es mucho más sencillo que el ejemplo del API DOM, imagínate que en lugar de texto fueran otros elementos como <h1>, <p>, otros <div>. Tendrías que crear elementos, agregarles el contenido y luego insertarlos dentro del elemento saludo.

Aun con este ejemplo, aunque es más sencillo que usar el API del DOM, se empieza a complicar cuando un elemento tiene muchos elemento hijos, por eso en React se recomienda usar JSX. Lo veremos a continuación.

React con ReactDOM y JSX

Ok, ya vimos como crear un elemento en React, pero ahora vamos a simplificar su implementación, JSX internamente utiliza React.createElement(component, props, ...children), pero estar llamando una función para crear un elemento complica la legibilidad del código.

Que tal si en lugar de escribir la función para crear un elemento, simplemente lo creas como cualquier otro codigo HTML, es mucho mas facil de escribir y leer, veamos la creación del elemento saludo con React y JSX.

const saludo = <div>Hola, soy un elemento saludo creado con React y JSX</div>;
const contenedor = document.getElementById('raiz-app');
ReactDOM.render(saludo, contenedor);

Mucha más fácil, ¿Cierto?, si te das cuenta no encerramos entre comillas el contenido del elemento saludo, JSX es lo suficientemente inteligente para entenderlo, pero para ellos es necesario descargar la herramienta llamada babel.js que lo hace posible.

Babel es una herramienta que no solo se encarga de convertir codigo JSX a invocaciones de React.createElement, sino que también convierte código con las últimas características del estándar ECMAScript a código que los navegadores web puedan entender. Para más información visita su sitio oficial.

Es importante aclarar que JSX internamente sigue siendo código JS, aunque parezca HTML y el uso de sintaxis HTML sea casi idéntica.

Usar babel para convertir JSX a JS

Todo muy bonito, pero para que podamos usar código JSX tenemos que descargar la herramienta.

<head>
 <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>

Al principio nuestro código lo insertamos dentro de las etiquetas <script>, pero para que babel pueda identificar que debe convertir codigo JSX a JS, es necesario decirle que es de tipo text/babel con el atributo type.

<script type="text/babel">
  const saludo = <div>Hola, soy un elemento saludo creado con React y JSX</div>;
  const contenedor = document.getElementById('raiz-app');
  ReactDOM.render(saludo, contenedor)
</script>

Ahora vamos agregar las propieades className y style para que tenga los mismos estilos que los ejemplos anteriores.

<script type="text/babel">
  const saludo = (
    <div
	  className="saludo"
	  style={{fontWeight: 'bold', textTransform: 'uppercase'}}
	>
      Hola, soy un elemento saludo creado con React y JSX
    </div>
  );
  const contenedor = document.getElementById('raiz-app');
  ReactDOM.render(saludo, contenedor)
</script>

Cuando se tiene más de una línea de JSX, es recomendado encerrarlo entre paréntesis para mejorar el entendimiento y evitar errores de JavaScript debido a la inserción automática de punto y coma.

Expresiones JS en atributos JSX

Algo interesante es como definimos el valor del atributo styles, usamos un par de llaves {}, todo lo que está encerrado en un par de llaves se ejecuta, por qué JSX lo identifica como una expresión de Javascript. En este caso JSX identifica que estamos definiendo un objeto para ser asignado a la propiedad style.

const saludo = (
  <div
  className="saludo"
  style={{fontWeight: 'bold', textTransform: 'uppercase'}}
  >
    Hola, soy un elemento saludo creado con React y JSX
  </div>
);

También podemos definir el objeto en otra línea y luego usar esa variable para realizar la asignación.

const styles = {fontWeight: 'bold', textTransform: 'uppercase'};
const saludo = (
  <div
  className="saludo"
  style={styles}
  >
    Hola, soy un elemento saludo creado con React y JSX
  </div>
);

El resultado de utilizar React con ReactDOM y JSX debe ser el mismo que en las últimas secciones, algo como la imagen de abajo.

Resultado React con JSX
Resultado React con ReactDOM y JSX

Componente reutilizable de React con ReactDOM y JSX

Ya que comprendemos la relación que tiene React con ReactDOM y JSX, es hora de que nuestro elemento saludo se convierta en un elemento reutilizable para saludar.

En Javascript el concepto de reutilización son las funciones, en React serian los componentes. Algo muy bueno de React, es que podemos hacer un componente usando una función. ¿Qué cosa puede ser más transparente que eso?

function Saludo(props) {
  return (
    <div
      className='Saludo'
      style={props.style}
    >
      {props.children}
    </div>
  );
}

Analizando un poco el código, vemos como la función Saludo, recibe un objeto de propiedades para ser usadas en el componente, recordemos que el contenido de este ejemplo se convierten en invocaciones a React.createElement(component, props, ...children).

En un ejemplo anterior vimos como asignar valores a las propiedades encerrando expresiones con llaves {}, no solo podemos hacer eso con atributos, sino también para pintar los elementos hijos, ejecutando {props.children}.

El código anterior JSX, se convierte en código JS, así:

function Saludo(props) {
  return React.createElement("div", {
    className: "Saludo",
    style: props.style
  }, props.children);
}

Ahora que tenemos un componente función, podemos usarlo de las siguientes formas.

const styles = { fontWeight: 'bold', textTransform: 'uppercase' };
const contenido = (
  <div>
   { Saludo({ children: 'Instancia de Saludo' }) }
   {
     Saludo({
       children: 'Instancia de Saludo en negritas',
       style: { fontWeight: 'bold' }
     })
   }
   <Saludo style={styles}>Instancia de Saludo con etiqueta</Saludo>
  </div>
);
const contenedor = document.getElementById('raiz-app');
ReactDOM.render(contenido, contenedor);

Lo mismo, todo este código se convierte a algo como lo siguiente.

function Saludo(props) {
  return React.createElement("div",
    {
      className: "Saludo",
      style: props.style
    },
    props.children
 );
}
const styles = { fontWeight: 'bold', textTransform: 'uppercase' };
const contenido = React.createElement(
  "div",
  null,
  Saludo({ children: 'Instancia de Saludo' }),
  saludo({
    children: 'Instancia de Saludo en negritas',
    style: {
      fontWeight: 'bold'
    }
  }),
  React.createElement(Saludo, { style: styles }, "Instancia de Saludo con etiqueta")
);
const contenedor = document.getElementById('raiz-app');
ReactDOM.render(contenido, contenedor);

Y el resultado visual es esto.

Resultado de usar un componente reutilizable con React
Resultado de usar un componente reutilizable con React

Date cuenta de que las invocaciones a la función Saludo, así como el uso de la etiqueta Saludo, se convierten en invocaciones a React.createElement(component, props, ...children). Y como el uso del JSX con la etiqueta <Saludo> facilita la lectura y creación del componente. Veamos un ejemplo solo usando etiquetas.

const styles = { fontWeight: 'bold', textTransform: 'uppercase' };
const contenido = (
  <div>
   <Saludo>Instancia de Saludo</Saludo>
   <Saludo style={{ fontWeight: 'bold' }}>Instancia de Saludo en negritas</Saludo>
   <Saludo style={styles}>Instancia de Saludo con etiqueta</Saludo>
  </div>
);
const contenedor = document.getElementById('raiz-app');
ReactDOM.render(contenido, contenedor);

¿Qué tal? Mucho más simple ¿Verdad?

Una última cosa, cuando uses etiquetas de los componentes de React, siempre deben empezar con la primera letra Mayúscula, así es como reconoce JSX que debe invocar a una función con ese nombre. De lo contrario se confundiría con las etiquetas normales de HTML y te lanzará un error como el siguiente.

Warning: The tag <saludo> is unrecognized in this browser. If you want to render a React component, start its name with an uppercase letter

Referencias

https://reactjs.org/

https://babeljs.io/

React: Métodos del ciclo de vida de un componente

React: Métodos del ciclo de vida de un componente

Introducción y diagrama

Antes de todo, estoy en el proceso de aprendizaje de React y en esta publicación voy a explicar, según mi entendimiento, los métodos del ciclo de vida de un componente con React.

A partir de la versión 16.3 de React se agregaron nuevos métodos del ciclo de vida de un componente para mejorar el rendimiento, buenas practicas y así obtener una mejor calidad de los componentes creados.

Principalmente este cambio es debido a componentes con funcionalidad asíncrona, esto es muy importante porque normalmente el mundo real es asíncrono y los componentes que creamos son utilizados en el mundo real por personas.

Por esta razón también se empiezan a dejar de utilizar los siguientes métodos, esto sucederá a partir de la versión 17 de React:

  • componentWillMount()
  • componentWillRecieveProps(nextProps)
  • componentWillUpdate(nextProps, nextState)

Dado que los anteriores métodos se dejaran de usar en la version 17, los siguientes métodos son los recomendados a utilizar en un componente:

Montado:

  • constructor()
  • static getDerivedStateFromProps(nextProps, prevState)
  • render()
  • componentDidMount()

Actualización:

  • static getDerivedStateFromProps(nextProps, prevState)
  • shouldComponentUpdate(nextProps, nextState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • render()
  • componentDidUpdate(prevProps, prevState, snapshot)

Desmontado:

  • componentWillUnmount()

Si ordenamos los métodos de manera secuencial:

  • constructor()
  • static getDerivedStateFromProps(nextProps, prevState)
  • render()
  • shouldComponentUpdate(nextProps, nextState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • render()
  • componentDidUpdate(prevProps, prevState, snapshot)
  • componentWillUnmount()

Y para visualizar su relación, aquí está un diagrama de flujo:

Diagrama de los métodos del ciclo de vida de un componente en React
Diagrama de los métodos del ciclo de vida de un componente en React

Si observamos el diagrama, el método static getDerivedStateFromProps(nextProps, prevState) sustituye al método deprecado componentWillReceiveProps(nextProps), también parece ser que el método getSnapshotBeforeUpdate(prevProps, prevState) sustituye al método deprecado componentWillUpdate(nextProps, nextState).

Para fines de demostración vamos a crear un componente Padre y otro componente Animal (componente hijo), lo ideal es que el componente Padre maneje todo el state, pero para demostrar el funcionamiento de los métodos ocuparemos algo de state en nuestro componente Animal.

constructor(props)

El constructor es un método de la mayoría de los lenguajes de programación orientada a objetos, y se utiliza para crear la instancia de una clase. En react el constructor se usa para crear la instancia de nuestro componente.

Cabe mencionar que después de la ejecución de este método, nuestro componente aún no se pinta en nuestro navegador, al proceso de pintado, es decir, insertarlo en el DOM, se le llama Montar o Mount en ingles.

Como buena practica de programación es importante ejecutar super() dentro de un constructor para que realice cualquier llamada a constructores padres.

En el caso de react se debe llamar con las props recibidas en el constructor, o sea, super(props), esto nos permite poder acceder a las props a través de this.props dentro del constructor.

El constructor se usa normalmente para las siguietes dos cosas:

  • Definir el estado local con un componente a través de this.state.
  • Para enlazar el objeto this(la instancia de nuestro componente) a los métodos que son utilizados en el método render(). Estos métodos son usados como manejadores de eventos

El estado de nuestro componente en el constructor se define así:

constructor (props) {
    super(props);
    this.state = {
        propiedad: 'Algún valor'
    }
}

Si no defines ningún estado en el constructor, entonces no lo necesitas.

Si se te ocurre definir el state usando las props pasados como parámetros probablemente es mejor definir el state en un componente padre o en la raíz de todos los componentes porque el estado no estará sincronizado con los cambios de las propiedades.

constructor (props) {
    super(props);
    this.state = {
        propieda: props.nombrePropiedad
    }
}

Para enlazar la referencia de la instancia de nuestro componente a los métodos que son utilizados en el método render() y que normalmente son los manejadores de eventos:

class Padre extends React.Component {
    constructor (props) {
        super(props);
        this.state = { src: '' }
        this.cambiarAnimal = this.cambiarAnimal.bind(this);
    }

    cambiarAnimal () {
        this.setState({
            src: 'Algúna url que apunte a una imagen de un animal'
        });
    }

    render() {
        return (
          <div>
            <Animal src={this.state.src}/>
            <button onClick={this.cambiarAnimal}>Cambiar animal</button>
          </div>
        );
    }
}

Ahora el método this.cambiarAnimal podrá acceder a la instancia de nuestro componente a través de this y así utilizar this.setState() para cambiar el estado.

Existe otra opción para utilizar métodos de nuestra clase como manejadores de eventos, con el uso de funciones flecha (arrow functions).

class Padre extends React.Component {
    constructor (props) {
        super(props);
        this.state = { src: '' }
    }

    cambiarAnimal = () => {
        this.setState({
            src: 'Algúna url que apunte a una imagen de un animal'
        });
    }

    render() {
        return (
          <div>
            <Animal src={this.state.src}/>
            <button onClick={this.cambiarAnimal}>Cambiar animal</button>
          </div>
        );
    }
}

static getDerivedStateFromProps(nextProps, prevState)

Este método es estático, sí, debe tener el modificador static que indica que este método no está enlazado a alguna instancia del componente, sino más bien a su clase. Se invoca después de instanciar un componente y también cuando el componente recibe cambios en las propiedades.

Debe tener siempre un valor de retorno, ya sea un objeto para actualizar el state o null si no se quiere actualizar el state en relación con los nuevos valores de las props recibidas. Es importante saber que este método se ejecuta también cuando un componente padre provoca que el componente hijo sea de nuevo renderizado, por esta razón debes comparar valores anteriores con los nuevos para evitar mandar a actualizar el state cuando no hubo realmente un cambio.

Podemos razonar que este método nos puede servir para mantener sincronizado nuestro state (o solo una parte) con las props pasadas desde un componente padre.

Por el momento en nuestro ejemplo del componente Animal solo visualizaremos los datos y regresaremos null porque no queremos actualizar el estado, además el atributo src de nuestra imagen se actualiza cuando la propiedad src del componente cambia.

...
static getDerivedStateFromProps (nextProps, prevState) {
    console.log('nextProps: ', nextProps);
    console.log('prevState: ', prevState);
    return null;
}

render() {
    return (
        <img className="cat__img"
          src={this.props.src}
          />
     );
}
...

Lo siguiente realmente no es necesario, pero para visualizar la ejecución de este método supongamos que dentro de nuestro componente Animal vamos a manejar la url(src) de la imagen del animal en this.state.src, así:

constructor (props) {
    this.state = {
        src: props.src
    };
}

static getDerivedStateFromProps (nextProps, prevState) {
    if (prevState.src !== nextProps.src) {
      // necesario para actualizar la imagen cada vez que cambie this.props.src
      return { src: nextProps.src };
    }

    return null;
}

render() {
    return (
        <img className="cat__img"
          src={this.state.src}
          />
     );
}
...

Ahora prueba el código aquí, y revisa los mensajes de la consola, por el momento solo nos estamos enfocando en el constructor(props) y static getDerivedStateFromProps(nextProps, prevState):

render()

Este método es obligatorio en cualquier componente, pues como su nombre lo dice, se utiliza para obtener los elementos finales a visualizar o pintar en el navegador. Debe ser una función pura, es decir, no debe modificar las props, no debe modificar el state ni realizar operaciones del DOM.

Según mi entendimiento, el resultado de este método es utilizado por ReactDOM.render() para insertarlo en el DOM del navegador. Si el componente en cuestión ha sido insertado previamente, solo se muta el DOM lo necesario para reflejar los nuevos cambios, esto quiere decir que render() regresa los objetos necesarios para que en otro lugar sean insertados en el DOM.

Esto se puede comprobar si observas la consola del anterior ejemplo y luego das clic sobre el botón “Cambiar animal” entonces verás que el método render() es ejecutado antes de getSnapshotBeforeUpdate() y componentDidUpdate().

Con esto tengo una duda, ¿En qué momento se modifica el DOM?, yo creo que se modifica el DOM después de ReactDOM.render() y antes de que componentDidUpdate().

...
render() {
    return (
        <img className="cat__img"
          src={this.state.src}
          />
     );
}
...

componentDidMount()

Este método se ejecuta cuando nuestro componente está listo en el DOM, siguiendo el razonamiento explicado en el método render(), se ejecuta después de que React inserte el DOM, y antes del método `render()`. Por eso es útil para realizar llamadas ajax y operaciones con el DOM como agregar eventos y/o modificar elementos internos.

Dentro de este método es seguro cambiar el state, pero si ejecutamos this.setState() provocara que nuestro componente se vuelva a renderizar.

La documentación oficial de React nos advierte tener cuidado con esto, pues puede causar problemas de rendimiento por renderizar nuestro componente varias veces. Sin embargo es necesario para los casos de tomar medidas y posiciones de algunos elementos antes de renderizar, por ejemplo el tamaño y posición de modales y tooltips.

Para ver el uso de este método veamos el siguiente ejemplo, si revisamos la consola veremos que render() se ejecuta dos veces, también si damos clic en el botón Cambiar animal, se nota que de nuevo render() se ejecuta dos veces.

¿Por qué sucede esto?, sucede porque dentro del método componentDidMount() agregamos un escuchador de eventos para la carga de la imagen del animal, al ejecutarse this.onImgLoad().

`this.onImgLoad()` invoca a this.setState() y esta función provoca que el componente se vuelva a renderizar para mostrar las medidas exactas de la imagen cuando se termina de cargar.

shouldComponentUpdate(nextProps, nextState)

En versiones actuales de React, este método se ejecuta para decidir si los cambios en las props o el state merecen que se vuelva a renderizar el componente con estos nuevos datos.

El valor de retorno de esta función es true o false. Si el resultado de este método es true, los métodos render(), getSnapshotBeforeUpdate() y componentDidMount()no se ejecutan.

Recibe como parámetros las nuevos valores pros y del state, con estos valores y los valores actuales de nuestro componente podemos condicionar si es necesario volver a renderizar o no, de esta manera podemos mejorar el rendimiento manualmente.

Si no implementamos este método en nuestro componente, React toma como resultado el valor true, por lo que siempre se volverá a renderizar. El resultado no influye en los componentes hijos, si en un componente padre el resultado es false, esto no impide que componentes hijos necesiten volver a ser renderizados.

Es importante mencionar que en la documentación indica que tal vez en futuras versiones de React, este método no impida un renderizado del componente, o sea, que en un futuro si este método regresa falseaun así se ejecutaran los métodos render(), getSnapshotBeforeUpdate() y componentDidUpdate().

Esto último me deja con un sabor amargo, actualmente este método es un buen lugar para mejorar el rendimiento de nuestro componente porque evitamos el re-renderizado en situaciones que no sean necesarias, pero después en futuras versiones cabe la posibilidad de perder esta habilidad, entonces quiero pensar que deben existir otras maneras de mejorar este caso de rendimiento, ¿Alguien tiene alguna idea?.

Veamos un ejemplo, en el anterior método componentDidMount() mencionamos que agregamos un escuchador de eventos al finalizar la carga de la imagen para poder obtener sus medidas, estás medidas las mostramos en nuestro componente, pero esta el caso de dos imágenes de perros que tiene la misma medida, 128px - 128px, entonces el ancho y el alto de la imagen no cambia, por lo que no es necesario volver a renderizar nuestro componente.

Si cambiamos de entre el perro de raza chihuahua y el perro ladrando, podemos ver en la consola que el último método ejecutado es el shouldComponentUpdate(), como el resultado fue false, no se ejecutaron de nuevo los métodos render(), getSnapshotBeforeUpdate() y componentDidUpdate().

getSnapshotBeforeUpdate(prevProps, prevState)

Este método se ejecuta después de render() y antes de componentDidUpdate(), el valor que regresa la ejecución de este método se convierte en el tercer parámetro llamado snapshot de componentDidUpdate(prevProps, prevState, snapshot), este método debe regresar algún valor o null, si no es así, React nos advertirá con algo parecido al siguiente warning en la consola:

Warning:
Animal.getSnapshotBeforeUpdate(): A snapshot value (or null) must be returned. You have returned undefined.

Además recibe las props y el state antes de la actualización por lo que es fácil hacer comparaciones con las props y el state actuales a través de this.props y this.state.

Este método puede regresar cualquier tipo de valor, por ejemplo puede regresar datos del DOM como cuantos elementos existen en una determinada lista de elementos o la posición del scroll antes de que el componente sea actualizado a través de ReactDOM.render()(si nuestra hipótesis explicada en el método render() es correcta).

Si revisamos el código de getSnapshotBeforeUpdate() y componentDidUpdate() y además revisamos la consola, notaremos que getSnapshotBeforeUpdate() le envía a componentDidUpdate el numero de elementos del DOM que contiene el componente en cuestión, que son 2, el párrafo con las medidas de la imagen y la imagen del animal.

componentDidUpdate(prevProps, prevState, snapshot)

Este método se ejecuta cuando el componente ha sido actualizado totalmente, y es reflejado en el DOM de nuestra aplicación, recibe las props y el state antes de la actualización por lo que es fácil hacer comparaciones con las props y el state actuales a través de this.props y this.state.

Aquí se puede trabajar con el DOM del componente dado que este mismo ha sido actualizado, además se puede realizar operaciones como obtener datos remotos según los cambios de las props o state.

No se ejecuta la primera vez que se usa el método render(), es decir, cuando se monta el componente.

En la consola podemos ver los datos, prevProps, preState, y snapshot, este último tiene el valor 2. También vemos this.props y this.state

componentWillUnmount()

Este método se ejecuta justo antes del que el componente sea desmontado del DOM, es un buen lugar para liberar recursos y memoria. Un ejemplo claro es la eliminación de escuchadores de eventos que ya no se van a necesitar, también se pueden cancelar peticiones remotas que se estén ejecutando actualmente dado que estas seguirán su proceso aún desmontando el componente.

En el ejemplo de abajo se ilustra como se elimina un escuchador de evento load para la carga de la imagen del animal.

es_MXES_MX