Como crear un buscador de criptomonedas con Redux, parte  1

How to create a cryptocurrency search engine with Redux, part 1

Introduction

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 here. 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 functions CryptoMarkets() {
  return (
    <>
      <SearchField />
      <Table />
    </>
  );
}

Componente SearchField

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

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

Componente Table

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

import styles from "./Table.module.scss";
export default functions 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 functions 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 functions 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 and 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 functions 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 functions 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.

The function 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 functions 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 of 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 and rows en lugar de importarlos del dummies.js

import styles from "./Table.module.scss";
import TableRow from "./TableRow";
export default functions 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 functions 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 functions 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 functions 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 functions CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  functions onSearchMarket(and) {
    dispatch({ type: "FILTER_MARKETS", payload: and.target.value });
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Se usa el “custom hook” useDispatch of 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

The 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 functions CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  functions onSearchMarket(and) {
    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 functions 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() and 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 functions 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 functions 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 functions 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 functions 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 and 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 functions CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  useEffect(() => {
    dispatch(getMarkets());
  }, [dispatch]);
  functions onSearchMarket(and) {
    dispatch(searchMarket(and.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 functions 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 functions 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 functions CryptoMarkets() {
  const dispatch = useDispatch();
  const headers = useSelector((state) => state.headers);
  const rows = useSelector((state) => state.filtered);
  useEffect(() => {
    dispatch(getMarkets(mapMarketsToUi));
  }, [dispatch]);
  functions onSearchMarket(and) {
    dispatch(searchMarket(and.target.value));
  }
  return (
    <>
      <SearchField onSearchChange={onSearchMarket} />
      <Table headers={headers} rows={rows}></Table>
    </>
  );
}

Y modificamos nuestro thunk de la siguiente forma.

export functions 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 functions 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.

Memoization, useMemo y useCallback

Memoization, useMemo and useCallback

Para comenzar a entender el uso de useMemo and useCallback, es necesario entender lo que es memoization either memoización en español.

¿Qué es memoization?

Memoization es una tecnica de optimización en el codigo para evitar repetir el trabajo computacional previamente realizado. Es por eso que la palabra memo, se refiere a poder recordar un resultado calculado anteriormente.

Tambien se puede entender como la forma de hacer cache de los resultados previos y evitar trabajo innecesario.

Memoization en funciones de Javascript

Cuando se trabaja con funciones en Javascript es fácil de implementar memoization, se puede usar closure o simplemente agregar una propiedad a la función (una función es un objeto). Esta propiedad normalmente puede ser un objeto o un array para guardar los resultados ya calculados.

Voy a escribir un ejemplo, nos quedamos con lo más simple agregando una propiedad memo a la función.

function multiplyByTen(number) {
  const memo = multiplyByTen.memo;
  const memoResult = memo[number];
  if (memoResult === undefined) {
    memo[number] = number * 10;
  }
  return memo[number];
}
multiplyByTen.memo = {};

Ahora si ejecutamos varias veces la función con algunas valores para number, los valores anteriormente calculados se extraen de la propiedad memo ahorrándonos de cálculos innecesarios.

multiplyByTen(5); // multiplica 5 * 10 = 50
multiplyByTen(6); // multiplica 6 * 10 = 60
multiplyByTen(5); // No se calcula y se obtiene el valor 50 de la propiedad memo

En el ejemplo expuesto multiplicar por diez es un cálculo sencillo, pero si fuera uno que necesite mucho más poder computacional de lo normal, sí que valdría la pena optimizar la función con memoization.

Memoization con React

En React la optimización con memoization es un poco diferente a lo que acabamos de ver, en React, solo se guarda el valor calculado anteriormente inmediato, y este se actualiza conforme se cambian los parámetros de la función.

Transformar conceptualmente el ejemplo anterior a la forma como React lo implementa, seria como el siguiente ejemplo.

function multiplyByTen(number) {
  const prevInput = multiplyByTen.prevInput;
  if (prevInput !== number) {
    multiplyByTen.prevInput = number;
    multiplyByTen.prevResult = number * 10;
  }
  return multiplyByTen.prevResult;
}
multiplyByTen.prevResult = null;
multiplyByTen.prevInput = null;

A continuación, al invocar la función con mismo parámetro que el anterior obtiene el valor previo, pero si los parámetros cambian, entonces si se hace el cálculo del resultado y este último se guarda como el prevResult. Así también con el parámetro que se guarda como prevInput.

multiplyByTen(5); // si hace el cálculo para regresar 50
multiplyByTen(5); // No hace el cálculo porque el parametro es el mismo, regresaa 50
multiplyByTen(6); // Si hace el cálculo, regresa 60
multiplyByTen(5); // Se hace el cálculo porque este parametro es diferente al anterior 5 !== 6

React.memo para componentes

En Javascript existen las funciones de alto nivel, en React existen las funciones que generan a los componentes de alto nivel o HOC (High Order Component). Las cuales reciben como parámetro un componente y regresan uno nuevo.

React.memo es una función que genera un HOC como resultado, pero además optimiza el renderizado de este componente HOC.

¿Cómo funciona React.memo?

Ya habíamos comentado que la memoization en React solo guarda los parámetros usados en la última invocación y el resultado anterior. Veamos un ejemplo de cuando se cambia el estado del elemento padre de un elemento creado con React.memo

// Primer render hace el cáculo necesario
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>
// Segundo render, ninguna propiedad cambia, no se hace otro cálculo
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>
// Tercer render, cambia firstName a "Juan", vuelve hacer el cálculo del elemento
<UserInfoMemo firstName="Juan" lastName="Cervantes"></UserInfoMemo>
// Cuarto render, se vuelve a cambiar firstName a "Jaime", hace otro cálculo del elemento
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>
// Quinto render, se cambia a lastName a "Velasco", hace otro cálculo del elemento
<UserInfoMemo firstName="Jaime" lastName="Velasco"></UserInfoMemo>
// Sexto render, ninguna propiedad cambia, no se hace otro cálculo
<UserInfoMemo firstName="Jaime" lastName="Velasco"></UserInfoMemo>

Example

El siguiente ejemplo se crea un componente llamado UserInfoMemo con la función memo y otro componente llamado UserInfo sin usar React.memo.

import { useState } from 'react';
import UserInfoMemo from './components/UserInfoMemo';
import UserInfo from './components/UserInfo';
functions App() {
  const [title, setTitle] = useState('Memoization');
  const [firstName, setFirstName] = useState('Jaime');
  const [lastName, setLastName] = useState('Cervantes');
  console.log('render App');
  return (
    <div className="App">
      <h1>{title}</h1>
      <UserInfo firstName={firstName} lastName={lastName}></UserInfo>
      <UserInfoMemo firstName={firstName} lastName={lastName}></UserInfoMemo>
      <button onClick={() => setTitle('Nuevo titulo')}>Nuevo titulo</button>
      <button onClick={() => setTitle('Otro nuevo titulo')}>Otro titulo</button>
      <button onClick={() => setFirstName('Juan')}>Nombre a Juan</button>
      <button onClick={() => setFirstName('Jaime')}>Nombre a Jaime</button>
      <button onClick={() => setLastName('Velasco')}>Apellido a Velasco</button>
      <button onClick={() => setLastName('Cervantes')}>Apellido a Cervantes</button>
    </div>
  );
}
export default App;

Cuando cambia el estado de title in App, UserInfo se vuelve a renderizar, pero UserInfoMemo no, este último solo se renderiza cuando su propiedad firstName cambia. Puedes probarlo presionando los botones de título que provocan cambios en el estado title y los botones Juan y Pepe que provocan cambios en el estado de firstName.

A continuación el código de <UserInfo> and <UserInfoMemo>.

components/UserInfo.js

export default functions UserInfo({ firstName }) {
  console.log('render');
  return (
    <div>
      <h2>UserInfo</h2>
      <p>firstName: {firstName}</p>
    </div>
  );
}

components/UserInfoMemo.js

import { memo } from "react";
const UserInfoMemo = memo(({ firstName, lastName }) => {
  console.log("Render UserInfoMemo");
  return (
    <div>
      <h2>UserInfoMemo</h2>
      <p>firstName: {firstName}</p>
      <p>lastName: {lastName}</p>
    </div>
  );
});
export default UserInfoMemo;

Puedes probar el ejemplo en codesanbox.io.

<to href="https://codesandbox.io/s/react-memo-hjq0w?fontsize=14&hidenavigation=1&theme=dark">
  <img alt="Edit React.memo" src="https://codesandbox.io/static/img/play-codesandbox.svg">
</to>

useMemo

El hook useMemo te permite recordar el valor anterior si los parámetros usados para el cálculo no cambian, tal cual implementa el concepto de memoization en React recordando solo el valor anterior.

La sintaxis es la siguiente.

useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);
// o así:
useMemo(
  functions() {
    return computeExpensiveValue(to, b);
  },
  [a, b]
)

Recibe una función para realizar un cálculo que involucre un gasto considerable de tiempo y recursos computacionales. Y como segundo parámetro una array de dependencias, para que solo se vuelva a ejecutar la función si alguna de las dependencias ha cambiado, de lo contrario regresa el valor recordado previamente.

const memoizedValue = useMemo(expensiveCallback, deps)

Ejemplo sin useMemo

Vamos a suponer que el arreglo de usuarios es muy grande y se obtiene a partir de la petición a un servicio, cada vez que se hace render de <App> se hace la petición y se genera el arreglo cuando cambiamos el estado de selectedUser

import { useState } from "react";
import UserList from "./components/UserList";
functions App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"];
  console.log('render App');
  return (
    <>
      <h1>useMemo</h1>
      <p>Usuario seleccionado: {selectedUser}</p>
      <UserList users={users}></UserList>
      <button onClick={() => setSelectedUser("Pedro Picapiedra")}>
        Cambiar usuario
      </button>
    </>
  );
}
export default App;

Y el componente <UserList>.

import { useEffect } from "react";
export default functions UserList({ users }) {
  console.log('render UserList');
  useEffect(() => {
    console.log('useEffect UserList');
  }, [users])
  return (
    <ul>
      {
        users.map(name => (
          <li key={name}>
            {name}
          </li>
        ))
      }
    </ul>
  );
}

En el primer render, los console.log se ejecutan como sigue.

render App
render UserList
useEffect UserList

Cuando se cambia el estado de selectedUser provoca otro renderizado de <App> y por consiguiente la asignación de la constante users se vuelve a ejecutar. Dado que se cambió el estado también se vuelve a renderizar <UserList>, hasta ahí no hay problema porque es lo que se espera. El problema es que se vuelve a ejecutar el useEffect in <UserList> debido a que la constante users contiene un nuevo array.

En segundo renderizado por cambio de selectedUser.

render App
render UserList
useEffect UserList

useMemo para users

Lo ideal es que el useEffect no se vuelva a ejecutar, porque los valores de los usuarios en realidad no cambiaron. Aquí es donde useMemo tiene utilidad conservando la referencia del valor anterior.

 const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
 );

De esta manera cuando cambiamos a selectedUser, nuestro useEffect ya no se ejecuta porque se sigue usando el mismo arreglo definido en App y no se vuelve a asignar uno nuevo gracias a useMemo.

render App
render UserList

Usando React.memo about <UserList>

Podemos optimizar UserList aún más, convirtiéndolo en un HOC usando la función React.memo.

import { useEffect, memo } from "react";
const UserListMemo = memo(UserList);
export default UserListMemo;
functions UserList({ users }) {
  console.log("render UserList");
  useEffect(() => {
    console.log("useEffect UserList");
  }, [users]);
  return (
    <ul>
      {users.map((name) => (
        <li key={name}>
          {name}
        </li>
      ))}
    </ul>
  );
}

Usando React.memo, cuando se presiona el botón para cambiar el userSelected, ya no se renderiza <UserList>. Ahora en la consola solo imprime render App.

render App

Imagínate que users debiera recibir un arreglo con mil usuarios. Vale la pena hacer memoization de esos datos para evitar volver a renderizar <UserList> con mil elementos <li> que representen a cada usuario. Solo es un ejemplo, no es que en aplicaciones reales quieras obtener mil usuarios de una sola vez, para eso están las paginaciones.

Agregando selectUser

El componente UserList necesita establecer el usuario seleccionado. Agregamos una función llamada selectUser que use setUserSelected para actualizar el usuario seleccionado.

import { useState, useMemo } from "react";
import UserList from "./components/UserList";
functions App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );
  console.log("render App");
  const selectUser = (user) => setSelectedUser(user);
  return (
    <>
      <h1>useMemo</h1>
      <p>Usuario seleccionado: {selectedUser}</p>
      <UserList users={users} onSelectUser={selectUser}></UserList>
    </>
  );
}
export default App;

Y en <UserList> tenemos lo siguiente.

import { useEffect, memo } from "react";
const UserListMemo = memo(UserList);
export default UserListMemo;
functions UserList({ users, onSelectUser }) {
  console.log("render UserList");
  useEffect(() => {
    console.log("useEffect UserList");
  }, [users]);
  return (
    <ul>
      {users.map((name) => (
        <li key={name} onClick={(and) => onSelectUser(name)}>
          {name}
        </li>
      ))}
    </ul>
  );
}

Ahora ya no tenemos problemas con la constante users gracias a useMemo y hasta evitamos el renderizado de <UserList> con React.memo.

useMemo para selectUser

Pero acabamos de agregar una función para actualizar el usuario seleccionado, cada vez que cambiamos el estado de selectedUser, la función se vuelve a crear y al ser una nueva referencia en memoria, entonces la propiedad onSelectUser of <UserList> cambia, por lo que si se renderiza este componente.

render App
render UserList

Para evitar el tema anterior también nos sirve useMemo, al permitirnos guardar un valor de cualquier tipo, también podemos guardar una función, mientras alguna de las dependencias no cambie, la referencia a la función seguirá siendo la misma entre renderizados.

import { useState, useMemo } from "react";
import UserList from "./components/UserList";
functions App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );
  console.log("render App");
  const selectUser = (user) => setSelectedUser(user);
  const memoizedSelectUser = useMemo(() => selectUser, []);
  return (
    <>
      <h1>useMemo</h1>
      <p>Usuario seleccionado: {selectedUser}</p>
      <UserList users={users} onSelectUser={memoizedSelectUser}></UserList>
    </>
  );
}
export default App;

Cuando cambiemos el estado de selectedUser, en la consola solo imprimirá lo siguiente.

render App

Puedes ver el código final aquí.

<to href="https://codesandbox.io/s/userlist-con-usememo-ehqrx?fontsize=14&hidenavigation=1&theme=dark">
  <img alt="Edit UserList con useMemo" src="https://codesandbox.io/static/img/play-codesandbox.svg">
</to>

Puntos importantes del código final

  • <UserList> se optimizó con React.memo
  • user and selectUserMemo han sido optimizados con useMemo.
  • Estas dos valores se pasan como propiedades a <UserList>.
  • The cosole.log in <UserList> (“render UserList” y “useEffect UserList”) solo se ejecutan al primer render porque users and selectUserMemo han sido optimizados con useMemo. Y <UserList> con React.memo.

useCallback

Anteriormente usamos useMemo para guardar la referencia de una función, es algo común que se quiera optimizar la creación de las funciones y evitar renderizados de componentes que reciban como propiedad esa función. Es por eso que existe useCallback, para facilitar la memoization de funciones.

Su sintaxis es como la que sigue.

useCallback(fn, deps);
useCallback(
  () => {
    doSomething(to, b);
  },
  [a, b],
);

Es el equivalente a usar.

useMemo(() => fn(a,b), [a, b)

En el ejemplo que hemos estado trabajando podemos reemplazar useMemo by useCallback.

const memoizedSelectUser = useCallback(selectUser, [])

El código final, finalito, final último de App, es igual al código de abajo y puedes revisarlo here

import { useCallback, useMemo, useState } from "react";
import UserList from "./components/UserList";
functions App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );
  console.log("render App");
  functions selectUser(user) {
    setSelectedUser(user);
  }
  // const memoizedSelectUser =  useMemo(() => selectUser, []);
  const memoizedSelectUser = useCallback(selectUser, []);
  return (
    <>
      <h1>useMemo</h1>
      <p>Usuario seleccionado: {selectedUser}</p>
      <UserList users={users} onSelectUser={memoizedSelectUser}></UserList>
      <button onClick={() => setSelectedUser("Pedro Picapiedra")}>
        Cambiar usuario
      </button>
    </>
  );
}
export default App;

Optimización prematura

Optimización prematura es la raíz de todo mal

Donald Knuth

Aunque estas optimizaciones con React.memo, useMemo and useCallback pueden ser importantes, por ejemplo cuando se renderizan muchos elementos debido a un cambio, como los mil usuarios que comentamos en secciones anteriores en el componente <UserList>. A menudo estas optimizaciones no se deben hacer prematuramente. El consejo de Donald Knuth no se refiere a no hacer optimizaciones por completo, sino hacerlas en el momento adecuado.

¿Pero cuándo es el momento adecuado?

En ambientes ágiles debemos recordar los términos YAGNI and KISS, que se refieren a construir las funcionalidades con el conocimiento que se tiene actualmente y de la forma más simple posible. Aplicando este principio de simplicidad podemos detectar en el futuro posibles optimizaciones, pero solo cuando sean detectadas y tengamos conocimiento que las necesitamos de verdad.

Si nos ponemos a analizar los ejemplos anteriores en realidad la mini aplicación no necesita estas optimizaciones y hacen que nuestro código sea más complejo. Se puede decir que para poder ejemplificar el uso de memoization en React incurrimos en una optimización prematura.

Con esto no quiero decir que no debas refactorizar tu código, claro que si, las pruebas unitarias y funcionales te permiten encontrar puntos de refactorización necesarios que nos ayudan a tener un código más limpio y entendible.

Precisamente el objetivo principal de la refactorización es el entendimiento del código que nos permitan mantener nuestras aplicaciones con el tiempo. Esto muy a menudo resulta en código con mejor rendimiento, pero solo el necesario.

Ya cuando se presente un punto de optimización considerable, donde el beneficio es más grande que tener u código más complejo, por supuesto que se puede implementar.

Conclusions

React.memo, useMemo and useCallback son útiles a la hora de optimizar código usando la técnica de memoization, solo se recomienda utilizarlos cuando se presentan cualquiera de estores tres puntos.

  • Cálculos que tengan un gasto considerable de tiempo y demás recursos computacionales como la memoria.
  • Guardar la referencia en valores para verificar cuando cambian. Principalmente cuando no son primitivos, como un arreglo, objeto literal o funciones.
  • Evitar renderizados de más cuando los valores que provocan el renderizado en realidad no han cambiado.

Con el uso de esta funciones de memoization en React debemos tomar mucho en cuenta lo que nos aconseja Donald Knuth. Los tres puntos anteriores son la guía para estas optimizaciones.

Optimización prematura es la raíz de todo mal

Donald Knuth

References

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

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

https://web.archive.org/web/20130731202547/http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf

useContext y useReducer. ¿Cómo replicar redux?

useContext and useReducer. How to replicate redux?

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

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

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

Contexto

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

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

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

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

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

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

createContext(defaultContext)

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

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

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

Ahora en nuestro archivo App.js importamos el contexto.

import UserNameContext from './UserNameContext';

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

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

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

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

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

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

static contextType = UnContexto

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

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

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

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

<UnContexto.Consumer>

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

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

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

useContext(UnContexto)

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

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

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

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

Aquí puedes ver el ejemplo completo:

Edit useContext(UserNameContext)

useReducer

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

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

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

Sintaxis

La sintaxis es la siguiente

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

Example

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

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

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

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

initialState e initiLazyFn

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

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

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

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

Función reducer

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

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

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

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

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

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

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

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

Edit useReducer(reducer, initState, initLazyFn)

Preparando codigo para combinar useContext y useReducer

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

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

actionTypes.js

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

actions.js

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

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

Función reducerUser

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

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

Uniendo todo en App.js

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

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

Mini redux con useContext y useReducer

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

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

Header y UserInfo

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

Archivo /components/Header.js.

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

Archivo /components/UserInfo.js

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

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

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

Crear contexto

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

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

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

Uso de useContext y useReducer para crear el store

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

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

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

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

Usando useContext para usar store en UserForm

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

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

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

Agregar <UserForm>

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

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

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

Aquí puedes ver el ejemplo completo.

Edit useContext y useReducer, replicando Redux

Conclusions

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

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

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

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

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

References

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

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

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

React Hooks Personalizados con useEffect

Custom React Hooks with useEffect

Ya se explicó como usar useEffect in 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 and 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 functions OldNewsPapers() {
  const [query, setQuery] = useState("");
  const [newsPaperNumber, setNewsPaperNumber] = useState(0);
  useEffect(() => {
    if (query) {
      console.log("fetch -> ", query);
      fetchOldNewsPapers();
    }
    async functions 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={(and) => 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.

Example

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 functions 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 functions 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={(and) => {
            setIsTyping(true);
            setQuery(and.target.value);
            console.log(
              `onChange: setIsTyping(true) y setQuery(${and.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 to false. Lo anterior causa otro repintado adicional, teniendo a isTyping equal to false and query equal to “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 and 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.

functions 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 functions 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.

functions useFetchOldNewsPaper(query, condition) {
  const [newsPaperCount, setNewsPaperCount] = useState(0);
  useEffect(() => {
    if (query && !condition) {
      doFetch()
    }
    async functions 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.

functions 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 came back 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

functions 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 functions 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 functions 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={(and) => {
            setQuery(and.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 and newsPapers, provocara otra invocación del componente función OldNewsPaper, por consiguiente también se invocaran las funciones useCanContinueTimeout and 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 came back 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.

Conclusions

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.

References

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 and 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. Functions in Node.js and JavaScript. What's really important
  2. Functions in Node.js and Javascript. More details

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 here and here. 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.

Better object composition over class inheritance

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 ${name}`;
  }, [name]);
}
// Esto si lo puedes hacer
useEffect(() => {
  if (name !== '') {
    document.title = `Algo con ${name}`;
  }
}, [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 functions RegistroDeUsuario() {
  const [name, setNombre] = useState("");
  return (
    <form>
      <label htmlFor="name">Name</label>
      <input
        type="text"
        value={name}
        id="name"
        onChange={(and) => setNombre(e.target.value)}
      />
      <button type="submit">Send</button>
      <section>{name}</section>
    </form>
  );
}

Y en el archivo App.js importamos el componente RegistroDeUsuario

import RegistroDeUsuario from "./RegistroDeUsuario";
export default functions 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 name and setNombre con la sintaxis de destructuring assignment.

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

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

Invocaciones y renderizados

Cada vez que se escribe sobre el campo Name, 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 name (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 name se actualiza a través del evento onChange and the method 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 name 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 functions RegistroDeUsuario() {
  const [name, setNombre] = useState(() => {
    console.log("Solo una vez");
    return localStorage.getItem("name") || "";
  });
  console.log("Más invocaciones");
  functions actualizarNombre(and) {
    setNombre(and.target.value);
    localStorage.setItem("name", and.target.value);
  }
  return (
    <form>
      <label htmlFor="name">Name</label>
      <input
        type="text"
        value={name}
        id="name"
        onChange={actualizarNombre}
      />
      <button type="submit">Send</button>
      <section>{name}</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 James, 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.

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

The function 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 name.

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

The function 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 and 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 this post.

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 functions 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={(and) => 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 here.

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 functions 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={(and) => setQuery(e.target.value)}
        />
        <button
          onClick={(and) => {
            and.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 functions 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 functions 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 functional programming 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!.

References

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 in Unsplash

Funciones en Node.js y Javascript. Más detalles

Functions in Node.js and Javascript. More details

“Funciones en node.js y Javascript. Más detalles”, es la segunda parte de lo más importante de las funciones en node.js y Javascript. El tema de funciones es muy importante, y también muy extenso. Aprenderás sobre el prototipo de una función, el comportamiento de la referencia a this, propiedad especial prototype, elevado de funciones y sus beneficios, variable adicional arguments y métodos útiles como call and apply. Y como extra muy importante, si llegas al final, las mejores recomendaciones de comunicación a través del código.

El prototipo de una función en Node.js y Javascript

En la primera parte de Funciones en Node.js y Javascript. Pudimos entender que las funciones son objetos, y el porqué las funciones son objetos. Una función tiene un prototipo, si no sabes que es un prototipo en Javascript, esto explica en this post.

Las funciones tienen como prototipo a Function.prototype. Aunque a diferencia de los objetos literales no se ven tan clara la relación, podemos ejecutar la siguiente prueba.

functions greet() {
  return 'Hola';
}
greet.__proto__ = function.prototype; // true

El prototipo de Function.prototype is Object.prototype. Con esta última sentencia comprobamos lo que hemos dicho de los objetos en Javascript, tienen como objeto base a Object.prototype.

greet.__proto__.proto === object.prototype; // true;

Aquí el diagrama.

Cadena de prototipos de una función
Cadena de prototipos de una función

Tipos de invocaciones y referencia this

Las funciones en Node.js y Javascript se comportan de manera extraña al ser invocadas. Este comportamiento cambia según la forma en como se invoque e influye en como se trata la referencia this. En las siguientes cuatro secciones se explican estos tipos de invocaciones.

Invocación de funciones en Node.js y Javascript como método

Cuando una función se asigna a una propiedad de un objeto entonces decimos que es un método. Y cuando un método es invocado se puede hacer referencia al objeto al que pertenece a través de la palabra reservada this.

Como vimos en el tema de objetos, se puede acceder a una propiedad a través de corchetes, así ['nombreDePropiedad'].

const James = {
  name: 'Jaime',
  greet: functions() {
    return `Hola soy ${this.name}`;
  }
};
James.greet(); // 'Hola soy Jaime'
jaime['greet'](); // 'Hola soy Jaime'

Invocación de funciones en Node.js y Javascript como función

Cuando una función se invoca de esta manera y quiere hacer referencia a this, este debería obtener null either undefined, pero en lugar de eso, hace referencia al objeto global del ambiente donde se está ejecutando el código, en el caso del navegador web hace referencia a window. En el caso de node.js es el objeto global.

functions greet() {
  return this;
}
greet(); // En el navegador es window y en node.js es global

Dentro del navegador web, es algo como lo siguiente.

Window {
  alert: ƒ alert()
  console: console {},
  fetch: ƒ fetch(),
  isNaN: ƒ isNaN(),
  clearInterval: ƒ clearInterval(),
  clearTimeout: ƒ clearTimeout(),
  setInterval: ƒ setInterval(),
  setTimeout: ƒ setTimeout()
  ...
}

Por otro lado en node.js es algo como esto.

<ref *1> object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  performance: [Getter/Setter],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [symbol(nodejs.util.promisify.custom)]: [Getter]
  }
  ...
}

Ahora, si una función interna hace referencia a this.

functions greet() {
  functions saludarInternamente() {
  	return this;
  }
  return saludarInternamente();
}
greet(); // En el navegador web es window, en Node.js es global

Esta función interna sigue haciendo referencia al objeto window either global. Y lamentablemente este mismo comportamiento sucede aun si la función greet es el método de un objeto.

const James = {
  name: 'Jaime',
  greet: functions() {
    functions saludarInternamente() {
      return this;
    }
    return saludarInternamente();
  }
};
James.greet();  // En el navegador web es window, en Node.js es global

Cuando el objeto se crea a través de una clase, la referencia a this is undefined, esto es una mejora, sin embargo, ¿Tendría más sentido que si hiciera referencia al objeto en cuestión? Yo opino que si, pero ese ejemplo vamos a guardarlo para cuando toquemos las clases.

Invocación de funciones en Node.js y Javascript como constructor

Javascript es un lenguaje de programación orientado a objetos basado en prototipos. Originalmente no usa clases, pero intenta simular una sintaxis basada en clases. Estamos hablando del operador new.

Las funciones constructoras son funciones que se invocan con el operador new para crear nuevos objetos. Como el ejemplo de abajo.

functions Persona(name, edad) {
  this.name = name;
  this.edad = edad;
}
const James = new Persona('Jaime', 33);

Esta forma de crear objetos con una función constructora permite que la referencia a this esté correctamente enlazada al nuevo objeto. Lo podemos comprobar cuando ejecutamos las siguientes líneas.

James.name; // 'Jaime'
James.edad; // 33

Propiedad especial prototype

Existe una propiedad especial llamada prototype en las funciones, esta no es la misma propiedad oculta que enlaza a Function.prototype.

Esta propiedad especial prototype permite usar a una función constructora para simular la herencia clásica. Así cuando se crea un nuevo objeto usando la función constructora, este nuevo objeto pueda usar las propiedades de prototype. Esto propiedad básicamente funciona igual que las propiedades Object.prototype, Function.prototype, Array.prototype, y demás prototipos de objetos en Javascript. Con la diferencia, que nosotros agregamos el contenido manualmente a prototype. A continuación un ejemplo.

functions Persona(name, edad) {
  this.name = name;
  this.edad = edad;
}
Persona.prototype.greet = functions () {
  return `Hola soy ${this.name} y tengo ${this.edad} años`;
};
Cadena de prototipos de una función y propiedad especial prototype
Cadena de prototipos de una función y propiedad especial prototype

Y con este ejemplo comprobamos que la función greet invocada desde el objeto James, efectivamente tiene la referencia a this enlazada correctamente. Y puede acceder al name and edad del nuevo objeto James.

const James = new Persona('Jaime', 33);
James.greet(); // Hola soy Jaime y tengo 33 años

Visualmente es como el siguiente diagrama.

Cadena de prototipos de un objeto creado con una función constructora
Cadena de prototipos de un objeto creado con una función constructora

Por último te preguntarás, ¿Por qué no agregamos los métodos dentro de la función constructora Persona? Muy parecido a lo que hicimos cuando vimos el tema de closure.

functions Persona(name, edad) {
  this.name = name;
  this.edad = edad;
  this.greet = functions () {
    return `Hola soy ${this.name} y tengo ${this.edad} años`;
  };
}
const James = new Persona('Jaime', 33);
James.greet(); // Hola soy Jaime y tengo 33 años

Pues la respuesta es que también se puede hacer, pero la desventaja es que estaríamos creando nuevas funciones cada vez que creamos una instancia de Persona. Con el uso de la propiedad prototype no se crean nuevas funciones, solo se hacen referencia a ellas, es decir, cada nuevo objeto debe ir a buscar en sus prototipos a la función saludar.

Nota importante sobre las funciones constructoras

Las funciones constructoras siempre empiezan con una letra mayúscula, no porque la sintaxis del lenguaje lo necesite, sino para que los programadores identifiquen a una función constructora de una que no lo es y se use adecuadamente el operador new.

Invocación de funciones en Node.js y Javascript con apply and call

Como ya hemos hecho mucho hincapié, las funciones en Node.js y Javascript son objetos. Por lo tanto una función también tiene métodos. Los métodos del prototipo Function.prototype.apply and Function.prototype.call, permiten definir explícitamente la referencia a this al ejecutarse el cuerpo de la función.

Invocación con Function.prototype.apply

const James = {
  name: 'Jaime',
  greet: functions(saludo) {
    return `${saludo}, soy ${this.name}`;
  }
};
James.greet('Hola'); // Hola, soy Jaime
James.greet.apply({ name: 'Pepito' }, ['Buenos días']); // Buenos días, soy Pepito

El método apply recibe como primer parámetro el objeto al que se va a enlazar la función y podrá hacer referencia usando this y como segundo parámetro un arreglo de valores que son los parámetros de la función. En este caso la función saludar solo recibe una cadena de caracteres a través del parámetro saludo. Es por eso que solo necesita un arreglo con un solo elemento.

Aquí un ejemplo con una función de dos parámetros.

functions sumar(to, b) {
  return to + b;
}
sumar.apply(null, [1, 2]); // 3

En este ejemplo el primer parámetro del método apply is null porque la función no se enlaza a un objeto y no necesita usar la referencia this.

Invocación con Function.prototype.call

Este método es una simplificación del método apply, igualmente recibe dos parámetros, el objeto al cual se hará referencia con this y los argumentos de la función que va a invocar. La única diferencia es que en lugar de recibir un array de parámetros para la función que se va a invocar, recibe cualquier número de parámetros separados con comas. A continuación un ejemplo.

functions sumar(to, b) {
  return to + b;
}
sumar.call(null, 1, 2); // 3

Parámetro extra arguments de las funciones en Node.js y javascript

Cuando una función es invocada, adicional a la lista de parámetros, tiene acceso a un parámetro llamado argument. Este parámetro extra contiene la lista de todos los parámetros pasados en la invocación de una función.

functions greet(complemento) {
  return arguments;
}
greet('Hola');
/*
Arguments [
  0: "Hola",
  callee: ƒ saludar(saludo),
  length: 1,
  Symbol(Symbol.iterator): ƒ values(),
  [[Prototype]]: Object
]
*/

arguments es un objeto muy parecido a un array, pero no lo es. En el ejemplo anterior vemos que su prototipo es Object.prototype y para ser un array debería ser Array.prototype.

Debido a lo anterior, arguments no tiene todos los métodos útiles de un array. Por ejemplo, que tal si queremos recorrer la lista de todos los parámetros, en un arreglo se podría usar el método Array.prototype.forEach, pero eso no es posible.

Una forma de mitigar este inconveniente es convertir el objeto arguments a un arreglo, usando el método Array.from. Y entonces si usar los métodos de Array.prototype que necesites. Ejemplo.

functions greet(complemento) {
  const args = array.from(arguments);
  args.forEach(actual => console.log(actual));
}
greet('Hola', 'Soy Jaime', 'Tengo 33 años');
/*
Hola
Soy Jaime
Tengo 33 años
*/

Aunque la función saludar solo recibe un parámetro, aun así todos los parámetros que se pasen en la invocación, se guardan en arguments.

Elevado de funciones en Node.js y JavaScript

El elevado se refiere a que la creación de función sube hasta el inicio del ámbito en donde se encuentre. Solo la declaración normal de función puede ser elevada. Ejemplo.

functions greet(complemento) {
  return agregarComplemento(complemento);
  functions agregarComplemento(complemento) {
    return `Hola, ${complemento}`;
  }
}
greet('soy Jaime'); // 'Hola, soy Jaime'

Como podemos ver la función agregarComplemento se puede utilizar antes de su definición, esto es porque javascript antes de ejecutar el código, crea la función al inicio del ámbito local donde se encuentra, el código anterior se traduce antes de ser ejecutado a lo siguiente.

functions greet(complemento) {
  functions agregarComplemento(complemento) {
    return `Hola, ${complemento}`;
  }
  return agregarComplemento(complemento);
}
greet('soy Jaime'); // 'Hola, soy Jaime'

Las funciones en forma de expresión y funciones flecha no cuentan con este elevado de funciones, así que es importante definirlas antes de ser usadas.

Beneficio del elevado de funciones

Este comportamiento parece confuso, pero cuando estás programando con funciones es muy útil. Cuando se programa se tiene funciones principales y funciones secundarias. Para la persona que lee nuestro código muy a menudo es suficiente con visualizar las funciones principales para entender el código, no tiene que perder el tiempo en ver los detalles de las funciones secundarias. Imaginate un archivo con cien líneas de código y compara encontrar las funciones principales en las líneas de inicio contra encontrar las funciones principales al final.

Veamos un ejemplo. Aquí al lector solo le interesa a alto nivel lo que renderiza renderVista.

functions renderVista() {
  renderHeader();
  renderMain();
  renderFooter();
}
functions renderHeader() {
  renderMenu();
  renderBanner();
  renderSearchForm()
  // ....
}
  functions renderMenu() {
    // ...
  }
  functions renderBanner()  {
    // ...
  }
  functions renderSearchForm()  {
    // ...
  }
functions renderMain() {
  renderContent();
  renderAside();
  // ...
}
  functions renderContent() {
    // ...
  }
  functions renderAside() {
    // ...
  }
  functions renderFooter()  {
    // ...
  }

Es facil, ¿Verdad?, solo leemos la primera función e ignoramos el resto del código.

¿Pero qué pasa cuando ahora la función principal se encuentra al final? Ejemplo.

functions renderMenu() {
  // ...
}
functions renderBanner()  {
  // ...
}
functions renderSearchForm()  {
  // ...
}
functions renderHeader() {
  renderMenu();
  renderBanner();
  renderSearchForm()
  // ....
}
functions renderMain() {
  renderContent();
  renderAside();
  // ...
}
functions renderContent() {
  // ...
}
functions renderAside() {
  // ...
}
functions renderFooter()  {
  // ...
}
functions renderVista() {
  renderHeader();
  renderMain();
  renderFooter();
}

Además de ir hasta el final para encontrar la función principal que nos interesa usar, se pierde el sentido de este archivo. O al menos tardamos más en entender el objetivo de este archivo, el cual es renderizar la vista.

Por último, si quisieras, puedes hacer que todo viva dentro de una sola función, utilizando funciones anidadas. Haciendo más claro la relación entre las funciones. Como el siguiente ejemplo.

functions renderVista() {
  renderHeader();
  renderMain();
  renderFooter();
  functions renderHeader() {
    renderMenu();
    renderBanner();
    renderSearchForm()
    // ....
      functions renderMenu() {
      	// ...
      }
      functions renderBanner()  {
        // ...
      }
      functions renderSearchForm()  {
        // ...
      }
  }
  functions renderMain() {
    renderContent();
    renderAside();
    // ...
      functions renderContent() {
          // ...
      }
      functions renderAside() {
        // ...
      }
  }
  functions renderFooter()  {
    // ...
  }
}

¿Las funciones son referencia o valor?

Como ya vimos en la publicación sobre objetos, dado que la función es un objeto, entonces una función siempre será una referencia.

Diferencias importantes entre funciones flecha y tradicionales

Las funciones flechas tienen ciertas limitaciones si las comparamos con las funciones tradicionales, aunque hay que decir que si tu código está enfocado a la programación funcional, este tipo de funciones son muy convenientes.

Sus limitaciones en comparacion con las funciones tradicionales son:

  • No tienen el enlace correcto a this y no puede usar super().
    • Debido al punto anterior no funciona bien con los métodos call, apply and bind.
    • No pude usarse como método de un objeto.
  • No tiene la propiedad especial prototype.
    • Consecuencia del punto anterior no es posible usar una función flecha como función constructora.
  • No tiene acceso a parámetro extra arguments.

Veremos estas limitaciones en una futura publicación sobre las funciones flecha.

Comunicación con funciones en node.js y Javascript

Como ya lo hemos mencionado en otras publicaciones, el código es una forma de comunicación entre lo que creamos ahora con nuestro yo del futuro y nuestro equipo. Sabemos que la programación es una actividad social de mucha comunicación y ser eficientes en esta comunicación ahorra tiempo y dinero a programadores y no programadores.

Recordemos que las funciones son las unidades más pequeñas de organización de código. Además sin las funciones, ¿Cómo definiríamos el comportamiento de objetos y de funciones más grandes? Las funciones nos sirven para:

  • Organize the code
  • Reuse code
  • It is used for the composition of objects, adding behaviors.
  • It is used for the composition of more complicated functions

Teniendo en cuenta lo anterior, he aquí algunas recomendaciones a la hora de crear funciones.

Un muy, muy buen nombre

En this post encontrarás las recomendaciones de nombres.

Lo más simple que se pueda

Una función debe ser lo más simple posible, y para eso debe tener una meta pequeña y específica, lo que conlleva a no complicar las cosas y hacer una cosa a la vez, paso a pasito, algo sencillo y bien hecho.

Cuando se resuelve un problema complicado, la forma recomendada de solucionarlo es como si imaginaras que eres un bebe, y tu problema es que no puedes correr. ¿Suena complicado no crees? Probablemente pudiste correr bien sin caerte alrededor de los tres años. Sin darte cuenta dividiste un problema grande en partes más pequeñas. Para llegar a correr, primero aprendiste a gatear, luego a caminar y finalmente intentaste correr con muchas caídas en el camino. Y si dividimos aún más estas tres fases encontraremos objetivos aún más pequeños, ejemplo, dar el primer paso. Así son las funciones, solucionan el problema pedacito por pedacito.

Siguiendo el enfoque anterior, una función simple es la que cumple con lo siguiente.

  • Se enfoca en resolver una sola cosa sencilla pero bien hecha.
  • Tendrá pocas líneas de código
  • Cuando mucho una o dos condiciones if o un switch
  • Cuando mucho uno o dos ciclos for, while, do while, etc.
  • Muy poquita indentación debido a los tres puntos anteriores
  • Por mucho tres parámetros, un humano solo puede recordar cuatro cosas a la vez, hagámosle la vida más fácil a nuestro compañero. Si de verdad necesitas muchos parámetros, puedes utilizar un objeto literal como único parámetro, así al menos no tenemos que recordar el orden en que deben de ir.
  • Todos los puntos anteriores lograrán que la función sea muy fácil de leer y comunicar.

En una futura publicación haremos una aplicación pequeña donde aplicaremos estas recomendaciones

Conclusions

El tema de funciones es muy grande, aún nos faltan temas por ver. Como nos seguimos dando cuenta, las funciones son una pieza fundamental en Javascript y en el desarrollo de software en general. Según donde se invoque una función, podrá enlazarse al objeto this correcto. El elevado de funciones es beneficioso para la comunicación de las intenciones de nuestras funciones.

No olvidemos que las funciones son objetos. Las recomendaciones expuestas reflejan el principio ágil de simplicidad. Manteniendo las cosas simples, somos más productivos. En el caso de nuestro código, aumentamos el poder de flexibilidad y mantenimiento del mismo a través de una comunicación clara y código fácil de comprender.

Funciones en Node.js y JavaScript. Lo realmente importante

Functions in Node.js and JavaScript. What's really important

Like Objects in Node.js and Javascript. What's really important. “Functions in Node.js and JavaScript. What's really important” refers to the principles behind functions, which is really important to continue learning, improve your understanding of functions in programming, Node.js and in Javascript.

Before talking about functions in Node.js and JavaScript, it is worth remembering or defining what a function is in programming.

What are functions?

Functions are a very important element in any programming language. We know that the functional programming was even invented before any programming language based on the lambda calculation of Alonzo Church.

Sintaxis calculo lambda, programación funcional
Lambda calculation syntax, functional programming. Image taken from https://www.slideshare.net/FASTPresentations/introduction-to-lambda-calculus-using-smalltalk-by-facundo-javier-gelatti

In the first programming languages, subroutines, procedures and functions were used. Subroutines, procedures and functions have in common that they group a set of operations with the purpose of reusing them many times and only writing them once.

Functions, unlike procedures and subroutines, are still used in modern programming languages, and are the smallest unit of code organization. They are used to define the behavior of objects, composing them with specific functionalities. Let's remember another post that the Objects are a group of functionalities that contribute to communication with other objects.

Without functions, an object wouldn't be of much use. Functions define the behavior of objects. And it also forms larger functions. In conclusion, the functions serve us for.

  • Organize the code
  • Reuse code
  • It is used for the composition of objects, adding behaviors.
  • It is used for the composition of more complicated functions

What are functions in Node.js and JavaScript?

In Node.js and JavaScript, and indeed in any environment where JavaScript is executed, functions are everything described in the previous section. And also They are objects.

  • Since they are objects, they can be treated like any other value:
    • They can be assigned to variables and properties of other objects
    • Create them dynamically during the execution of the JavaScript code
    • Have its properties and methods
    • Be parameters for another function
    • Be the return value of a function
  • Additionally, the body of a function provides local scope to variables and parameters.

Although it is not the topic of this post, all the features listed make JavaScript can be used as a functional programming language.

How to create functions in Node.js and JavaScript?

The three recommended ways to create functions are as follows.

  • Normal declaration
  • Function as expression
  • Arrow functions

Function declaration

This is the most common method, very similar in other programming languages. The reserved word is used functions, followed by the function name, then a list of arguments in parentheses, which are separated by commas. This argument list is optional.

Finally the function body using braces { }. The body of the function contains the statements you need.

functions name(argumento1, argumento2, ...argumentoN) {
  // sentencias
}

Concrete example:

functions sumar(to, b) {
  return to + b;
}
sumar(1, 2); // 3

To execute the function code it is necessary to invoke it. The invocation is made with a pair of parentheses and the necessary arguments inside, separated by commas. Just like the previous example.

A function always returns some value, even if it is not explicitly defined. If you do not define what a function returns, by default the return value will be undefined.

functions sumarSinReturn(to, b) {
  const resultado = to + b;
}
sumarSinReturn(1, 2); // undefined

If we execute the function, it returns undefined because we don't explicitly tell it to return some value

Function in expression form

This way of creating functions is much more flexible, its definition can appear wherever an expression can be defined. That gives it the ability to be assigned to a variable or a property of an object.

Its syntax is the same as the declaration of a function that we saw previously, but when assigned to a variable or property, its name is optional.

functions [nombreOpcional](argumento1, argumento2, ...argumentoN) {
  // sentencias
}

Next we will see some examples.

// Sin nombre, tambie  conocida como funcion anonima
const sumar = functions(to, b) {
 return to + b;
};
// Con nombre
const sumar = functions sumar(to, b) {
  return to + b;
};
const calculadora = {
  sumar: functions(to, b) {
    return to + b;
  }
};
const person = {
  // como propiedad de un objeto
  eat: functions() {
    return 'Comiendo...';
  }
};

Arrow functions

Arrow functions are the newest way to create functions, much more similar to mathematical functions in algebra. They feel very convenient because their syntax is much more reduced. They are an alternative to functions in the form of expressions, they are faster to write. However, it has many limitations compared to the other two ways of creating functions. Although if you use them for functional programming they are quite effective.

To be honest, it seems to me that if its use is not focused on functional programming, it does add more complexity to the use of functions in JavaScript, in itself the functions in Node.js and Javascript can be very different compared to other languages.

But well, let's look at its syntax.

argumento => expresión;
// Com mas de un arguento es necesario parentesis
(argumento1, argumentN) => expresión;
// Con varias lineas de sentencias, es necesario las llaves {}
argumento => {
 // sentencias
};
// Con mas de un argumento y con varias lineas se sentencias
(argumento1, argumentoN) => {
  // sentencias
};
// Sin argumentos es necesario los parentesis
() => expresión

When using expressions, it is not necessary to explicitly define the return. The result of the expression is the return value.

const calcularCuadrado = to => to * to;
const sumar = (to, b) => to + b;
const greet = () => 'Hola';
// invocaciones
calcularCuadrado(5); // 25
sumar(1, 2); // 3
greet(); // 'Hola'

When we want the body of the function to have several lines of statements, curly braces are used. Furthermore, if we want the function to return some value, then we explicitly use the syntax of return.

Examples.

const calcularCuadrado = to => {
  const result = to * to;
  return result;
};
const sumar = (a, b) = > {
  const result = to + b;
  return result;
};
// invocaciones
calcularCuadrado(5); // 25
sumar(1, 2); // 3

Nested or internal functions

A function can be defined inside another function, that is, we can dynamically create internal functions inside another main function and invoke them.

functions greet() {
  functions saludarInterna() {
    return 'Hola';
  }
  const saludo = saludarInterna();
  console.log(saludo);
}
greet(); // 'Hola'

In the next section we will see other nested functions.

Local scope of functions

Functions in Node.js and JavaScript provide a scope of values, local to the body of the function, that is, what is defined in the body of the function can only be referenced within the function.

functions grandfather() {
  const name = 'Jaime';
  const last name = 'Cervantes'
  functions father() {
    const last name = 'Good day';
    functions son() {
      const last name = 'Pérez';
      const nombreCompleto = `${name} ${last name}`;
      console.log(nombreCompleto); // Jaime Pérez
    }
    son();
    const nombreCompleto = `${name} ${last name}`;
    console.log(nombreCompleto); // Jaime Buendía
  }
  father();
  const nombreCompleto = `${name} ${last name}`;
  console.log(nombreCompleto); // Jaime Cervantes
}
grandfather();

Internal functions can access the variables of their parent function (so to speak). In the previous example you can see that the function sonor you can refer to the constant name of the function grandpaeither. This produces us Jaime Perez. In the next section we explain it better

Closures or closures

Function nesting allows child functions to have their own local scope, hidden from parent functions. At the same time these internal functions have access to the values defined in the parent functions. This encapsulation of information and at the same time access to external information is called closure.

Let's continue with the example from the previous section, the functions grandfather, father and son

functions grandfather() {
  const name = 'Jaime';
  const last name = 'Cervantes'
  functions father() {
    const last name = 'Good day';
    functions son() {
      const last name = 'Pérez';
      const nombreCompleto = `${name} ${last name}`;
      console.log(nombreCompleto); // Jaime Pérez
    }
    son();
    const nombreCompleto = `${name} ${last name}`;
    console.log(nombreCompleto); // Jaime Buendía
  }
  father();
  const nombreCompleto = `${name} ${last name}`;
  console.log(nombreCompleto); // Jaime Cervantes
}
grandfather();

The result of the function invocation grandfather is:

Jaime Pérez --> From son function Jaime Buendía --> From father function Jaime Cervantes --> From grandfather function

The more internal the function, the more scope it has to all the areas of the other “external parent” functions. Like the image below, it is as if the function scopes grandfather and father were within the scope of the function son.

Closure, ámbito de funciones en js
Closure, function scope in js

A function will always take the value of the variable that is closest to its own local scope. The variables within their own local scope are the most relevant. This allows variable and constant names to not collide between nested scopes.

The function son, has access to the constants name and last name of the function grandfather. You also have access to the constant last name of the function father. But the constant last name within the function itself son is closer than defined in father and grandfather, has greater relevance. So the full name that is printed to the console is Jaime Perez instead of Jaime Buendía or Jaime Cervantes.

The function father if you have access to the constants name and last name of the function grandfather. In its own sphere it has a constant last name equal to Good day. As this value is closer, it does not take the last name of the function grandfather which is further away. That is why in the console the full name that is printed is Jaime Buendía. Then the function father You do NOT have access to the constant last name of the function son.

Finally it prints to the console Jaime Cervantes Velasco because the constants name and last name They are defined at the local level of the grandfather function. The grandfather function does NOT have access to constants last name of its internal functions father and son.

OOP emerged from functions

Now that we've seen a little about nested functions and closures, we can talk about how object-oriented programming was discovered. This reflects the importance of functions in programming languages.

Ole Johan Dahl and Kristen Nygaard realized that the function call stack in ALGOL could be moved to a Heap. This allows variables declared by a function to exist even after the function finishes executing and returns some value.

In this way the function became the constructor of the class, the local variables became properties of the class instance, and the internal functions became its methods. And so in 1966 object-oriented programming was discovered.

We can implement this behavior using functions in Node.js and JavaScript and taking advantage of their ability to create closures.

functions createPerson(name, last name) {
  functions greet() {
    return `Hola soy ${name}...`;
  }
  functions eat() {
    return 'Comiendo...';
  }
  functions getName() {
    return `${name} ${last name}`;
  }
  const instancia = {};
  instancia.greet = greet;
  instancia.eat = eat;
  instancia.getName = getName;
  return instancia
}
const James = createPerson('Jaime', 'Cervantes');
James.eat(); // Comiendo...
James.greet(); // Hola soy Jaime
James.getName(); // Jaime Cervantes

The parameters name and last name, are within the local scope of the function createPerson, so they work just like variables inside the function body. The inner functions continue to have access to those parameters even after the parent function returns its value, an object literal that is the instance of a person.

Then when the instance James invokes its method getName, this property refers to the internal function getName of the function createPerson. Due to internal function closure getName, we have access to the parameters name and last name even long after the parent function createPerson has returned its value.

Function Names in Node.js and Javascript

We have to be aware that programming and software development is a social activity, with a lot of communication. And the more efficient this communication is, the greater the success of the software. This allows us to save the time and financial resources of everyone involved. I'm talking about programmers and non-programmers, investors, clients and users.

One of the forms of communication between fellow programmers and often oneself in the future is through easy-to-understand code, and to contribute to this easy understanding we must choose the name of our functions very carefully.

Take into account the following recommendations. But keeping in mind that these are only examples, and when it comes to writing your real functions and with the proper context, you will most likely be able to come up with better function names than those shown here.

Spend enough time naming your role.

Just as important as naming the variables is the functions. Functions are the smallest units that allow us to define behaviors in applications. The time spent naming your functions is much less than the time you have to spend later on yourself and your colleagues trying to figure out what a function actually does. It's like organizing your room, the tidier it is, the faster you will find the things you need, the faster you will change, or the faster you will find your socks, etc.

The name must be very semantic, describe its objective

The name of a function should describe as clearly as possible what it does. It is important that be a verb because a function always performs one or more operations focused on a specific task.

For example, if a function returns the full name of a person, which of the following names fits best?

fullName()
getName();
getFullName()

The name that best describes the purpose of the function is getFullName.

If the function returns a boolean, the function name must indicate that the result can be true or false. Just like the result of a logical condition. It's like asking a question whose possible answers can only be yes or no.

hasChildren(person) {
  return Boolean(person.childre.length);
}
if (hasChildren(currentPerson)) {
  // Haz algo
}

Yeah hasChildren If it were a method, it would look like this.

if (currentPerson.hasChildren()) {
  // Haz algo
}

You notice how the condition reads like a very understandable phrase. If currentPerson has children, then...do something.

Avoid wrong assumptions

If the name describes things it doesn't actually do, then we should rename that function. For example, if a function forms the full name of a customer and returns that name. Which feature best prevents erroneous assumptions?

functions setClientName() {} // Se entiende que el nombre del cliente va a ser modificado
functions getFullClientName() {} // Aquí esta claro que solo consigue el nomnbre completo del cliente

setCustomerName It tells us that the client's name will be changed, it is a bad name. So the name that best avoids erroneous assumptions is getFullClientName. It does not say what forms the name, but it does say that it will return a full name. For practical purposes we are not interested in knowing how that full name is formed, just that it does not return them.

Agreements with programmers

It is very important to establish agreements for the appointment of functions. In the previous examples I have been using the prefix get which indicates that I send to obtain something. But it would be confusing if some programmers used the word fetch and others the word retrieve, and others collect either bring.

Use the right context

It is important to understand the context of the function, in previous examples we used the function getFullClientName, but depending on the context of the application, it might be better getFullUserName either getFullEmployeeName.

Although these names have to do with the context of the business or problem, there are also technical terms that programmers are already very accustomed to and should not be mixed with the problem domain.

For example, the observer design pattern contains methods like update, subscribe, publish, notify. If we are working with a magazine application that uses native cell phone notifications, SMS, and makes periodic publications. This can also create confusion, so you should name functions very carefully in such a way that you distinguish between functions or methods of the design pattern and others related to the business.

Function scope helps name length

The name of the functions can be long or short depending on the scope it has in the software. For example, a function that is used a lot in different files is worth keeping its name short. Because if many people use it, it is important that it is easy to write and pronounce.

On the other hand, if it is a function that is only used in a file, its name can be long, these types of functions are normally used internally by other functions with short names. So long name functions are a way to explain what the short name function does. This doesn't mean it can't be short, but if you need more words to better describe the feature, go ahead.

As an example, let's imagine a function that returns the total of your profits to the current date of your entire investment portfolio. Where the profits are the sum of the interest on your investments plus the contributions you have made to date.

// funcion corta, reutilizable en otros archivos o aplicaciones
functions getEarnings() {
  return calculateEarnings();
}
// funciones con nombre más largo que describen a la funcion corta
functions calculateEarnings() {
 const earnings = calculateCurrentTotalInterest();
 const aditionals = calculateCurrentTotalAdditionalContributions();
  return earnings + aditionals;
}
functions calculateCurrentTotalInterest() {}
functions calculateCurrentAdditionalContributions() {}

Don't worry so much about these examples, the goal is to give you an idea. In a future publication we will make a small application where we will see how to apply these recommendations.

Conclusions

Functions in Node.js and Javascript is a fairly broad topic, it is one of the best done things in JavaScript that reveal the power of the language. It is noted that Javascript is influenced by LISP and Scheme.

Likewise, let's not forget to name our functions correctly, they are the smallest units that allow the software to be organized into well-defined behaviors.

Functional programming is the first programming paradigm invented, hence the importance of functions, because it is a paradigm from which object-oriented programming took its bases.

Let's not forget that functions in Node.js and JavaScript are objects and therefore can be treated like any value.

We are still missing several important feature topics. These are addressed in this post. If you have any questions, do not hesitate to write them in the comments, we will be happy to help you!

If you want, as an exercise, you can translate all the examples into functions in the form of expression and arrow functions. Have fun!

Objetos en Node.js y Javascript. Lo realmente importante

Objects in Node.js and Javascript. What's really important

Introduction

Objects in Node.js and Javascript. What's really important” refers to the principles behind objects, which is really important to continue learning, improving the understanding of objects in programming and in Node.js and Javascript.

Let's start with understanding what an object is in programming.

What are objects in programming?

It has been said a lot that object-oriented programming represents the real world. The truth is that programming in general is about representing the real world in something digital. So object-oriented programming is also about digitally representing the real world, just with a little more emphasis on the use of objects. But as we have established in this post, the important thing is NOT the objects, the important thing is:

Message passing for communication between objects

In programming a object It is a digital form of group features and that some of them may be analogous to an object in real life, but that it is much more limited and should not be expected to be the same, because it is only a digital representation.

When you develop software, and you need to add functionality, modify or fix a bug, your logical thinking is based on representation of digital objects, their relationships and communications with other digital entities. You should never think that as the real object behaves in a certain way, so does its representation. It is very helpful conceptually, but in implementation they are very often different.

Let's remember what Alan Kay tells us:

I'm sorry I coined the term a long time ago. Objects for programming because it made people focus on the least important part. The big idea is "Sending messages«

Alan Kay

Also a object serves for store data useful that other objects can use. So an object serves us to:

  • Create communications between digital representations of objects.
  • It allows grouping functionalities related to the digital representation of the object.
  • Stores data that another object can use. (It works like some data structures.)

What are objects in Node.js and JavaScript?

The objects in Node.js and Javascript, they are a collection of pairs name: value, similar to PHP's "associative arrays." These pairs of name/value They are called properties, a property is like a variable and can contain any type of value.

The key to the previous paragraph is that it can contain any type of value, including functions. And with this two types of objects are identified.

  1. Objects with functionalities and communications
    1. Which for the most part have methods to communicate with other objects and solve problems.
  2. Objects with data. Also called data structure
    1. Which for the most part contains data and which helps us to store and send information through messages. These objects are used by objects with functionalities and communications.

Let's remember that everything related to objects also works anywhere JavaScript is executed, node.js including.

How to create objects in Node.js and Javascript?

The simplest way to create an object is through an object literal. A literal object is one that is created with curly braces {}, adding its properties and methods. Like the following example.

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  }
};
James.getName(); // Jaime Cervantes Velasco

We have an object with five properties, name, last name and mother's last name are of type string, age is type number and the last property, getName() is type functions. Here you have more information about data types.

Although we will see the functions in detail later, for now, we can say that the functions allow us to encapsulate and execute a set of operations that we do very often. Instead of repeating the code of these operations many times, we better put these operations inside a function.

Whenever we need to execute these operations, we simply execute the function, this execution of the function is called invocation. Example of invocation is in the last line, where we use parentheses, jaime.getName();.

How do I get the value of a property?

To obtain some property of an object, we can do it in two ways:

  • Dot notation
  • Using square brackets, much like accessing data in a array

The first way, dot notation, is very simple, example:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  }
};
James.name // 'Jaime'
James.last name; // 'Cervantes'
James.mother's last name; // 'Velasco'
James.edad; // 33
James.getName // function ()  { return 'Jaime Cervantes Velasco'; }

The second way, using square brackets [], example:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  }
};
jaime['name'] // 'Jaime'
jaime['last name']; // 'Cervantes'
jaime['mother's last name']; // 'Velasco'
jaime['edad']; // 33
jaime['getName'] // function ()  { return 'Jaime Cervantes Velasco'; }
jaime['getName']() // 'Jaime Cervantes Velasco'

Here we access the properties with square brackets using the name of the properties, which are strings.

Highlights how we invoke the function getName. These two statements do the same thing, invoke the function getName:

jaime.getName(); jaime['getName']();

Property names are character strings

When we use square bracket notation, we use a string of characters to get the value. What we mentioned before about An object is a collection of pairs name: value. He name of all properties are strings.

Since property names are strings, we can define properties enclosed by quotes, like the following example:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  },
  'nueva-propiedad-con-guion-medio': 'Mi propiedad con guión medio encerrada por comillas'
};
jaime['nueva-propiedad-con-guion-medio']; // 'Mi propiedad con guión medio encerrada por comillas'

And get them with bracket notation like this, jaime['new-property-with-middle-hyphen'].

A property like this cannot be accessed with dot notation, so jaime.new-property-with-script.medium. This statement will give you an error in Javascript because it is not a valid property name.

In fact in Javascript the name of valid variables or properties must:

  • Start with a letter, included $ and _.
  • It cannot begin with numbers or characters used in the language for other purposes, e.g. -, %, /, +, &.
  • After the starting letter you can use numbers, other valid letters, $ and _.

Objects in Node.js and Javascript that contain other objects?

An object can contain any type of data, so it can contain other objects. Let's look at an example:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  },
  address: {
    calle: 'Melchor Ocampo',
    numero: 2,
    colonia: 'Las Flores',
    municipio: 'Tezonapa',
    estado: 'Veracruz'
  }
};

Here we add an object address to the object James. Simply using curly braces to create object literals.

How do I add, modify and delete properties?

Once you create an object, you can add more properties, modify them, and delete them.

Add properties to objects in Node.js and Javascript

Assignment is used to add new properties that do not previously exist.

const James = {
  name: 'Jaime',
};
James.last name = 'Cervantes';
James.appelidoMaterno = 'Velasco';
console.log(jaime ); // { nombre: 'Jaime', apellidoPaterno: 'Cervantes', apellidoMaterno: 'Velasco }

Update properties to objects in Node.js and Javascript

Assignment is also used to update the value of properties that already exist.

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
};
James.last name = 'Perez'
James.mother's last name = 'Moreno';
console.log(jaime ); // { nombre: 'Jaime', apellidoPaterno: 'Perez', apellidoMaterno: 'Moreno' }

Remove properties from objects in Node.js and Javascript

To delete properties, use the operator delete. Example:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
};
delete James.last name;
// o tambien así:
delete jaime['apellidoMaterno];
console.log(jaime); // { nombre: 'Jaime' }

The properties last name and mother's last name, no longer exists in the object James.

Are objects in Node.js and Javascript a reference or a value?

Objects in Node.js and JavaScript will always be a reference, that is:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
};
const Pedro = James;
Pedro.name = 'Pedro';
James.name === Pedro.name; // true;
James.name; // 'Pedro'
Pedro.name; // 'Pedro'

The constants James and Pedro they refer to same object in memory.

How do I cycle through the properties of an object?

There are several ways, the simplest to go through the properties of an object is the following.

We have the object James:

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  }
};

The first thing is to obtain the name of its properties with the method Object.keys, which generates a array with the names of the properties.

const namesProps = object.keys(jaime);
console.log(nombresProps); // ["nombre", "apellidoPaterno", "apellidoMaterno", "edad", "getNombre"]

Now we go through the arrangement namesProps to get the values.

namesProps.forEach(nombreProp => {
  console.log(James[nombreProp]); // imprime el valor del nombre de propiedad actual
});

The result of the above code is something like the following.

'Jaime'
'Cervantes'
'Velasco'
33
functions () {
  return 'Jaime Cervantes Velasco';
}

The prototype of an object?

Javascript is a multi-paradigm programming language, On its object-oriented side, it does not use classes for code reuse, rather it uses object composition. But how do you do this composition of objects?

Better object composition over class inheritance

Better object composition over class inheritance

Design Patterns: Elements of Reusable Object-Oriented Software

This principle is widely applied in Javascript and therefore applies in the same way in Node.js, in fact that is how it was built. Every object in Javascript has a link to a prototype, and can use all the properties and functions of its prototype.

This prototype link can be referenced in code, using the special property __proto__.

In the case of literal objects, such as James, are linked to Object.prototype, which is the base object of all objects in Javascript.

const James = {
  name: 'Jaime',
  last name: 'Cervantes',
  mother's last name: 'Velasco',
  edad: 33,
  getName: functions () {
    return 'Jaime Cervantes Velasco';
  }
};
James.__proto__; // { constructor: f Object(), hasOwnProperty: f hasOwnProperty, ... }
James.__proto__ === object.prototype; // true
James.toString === object.prototype.toString; // true
James.toString(); //[object Object]

If we expand the prototypes in a web browser like chrome, it is displayed like this.

Objeto literal tiene como prototipo a Object.prototype
Object literal jaime has Object.prototype as its prototype

Graphically, the prototype link looks like the image below.

Object.prototype el prototipo de jaime
Object.prototype jaime's prototype

You can also see the prototype at node.js if you link it to a debugger. But we won't see how to do that today.

What is the prototype chain?

We have created the object James literally, but what if we create it through another object person, making the person object its immediate prototype.

const person = {
  greet: functions () {
    return 'Hola';
  },
  eat: functions () {
  	return 'comiendo...';
  }
};
const James = object.create(persona); // Creamos jaime en base al prototipo persona

If we now do a console.log(jaime):

console.log(jaime);

And we expand the result in a web browser like Chrome, the result is displayed as follows.

Cadena de prototipo
prototype chain

In the image, we can see that [[prototype]] points to the object person, hence James has access to the methods eat and greet of the object person.

And if we make these comparisons we see that the link to the person prototype exists.

James.__proto__; // { saludar: f (), comer: f () }
James.__proto__  === person; // true

We can invoke the methods eat and greet of the person object as if they were James.

James.eat();  // 'Comiendo...'
James.greet(); // 'Hola'

Now if we expand the last [[prototype]] What do you think the result will be?

Cadena de prototipos Object.prototype
Object.prototype prototype chain

We clearly see that the last [[prototype]] is Object.prototype, the base prototype of all objects in Javascript. And you can check it in code by comparing Object.prototype with special properties __proto__.

James.__proto__.__proto__ === object.prototype; // true

We can also use the method toString as if it were from James.

James.toString(); // [object Object]

This is called a prototype chain., an object can have the necessary sub prototypes to reuse its properties and methods. In this case it was like this, jaime → person → Object.prototype.

jaime --> persona --> Object.prototype
jaime –> person –> Object.prototype

Peculiarities with the prototype chain

Use the methods and properties of sub prototypes

The object James you can directly use the properties and methods of your prototypes.

James.toString(); // '[object Object]' --> de Object.prototype
James.hasOwnProperty('name'); // true --> de Object.prototype
James.greet(); // 'Hola' --> de persona
James.eat(); // 'Comiendo...' --> de persona
James.name; // 'Jaime' --> de si mismo, objeto jaime

Updating properties does not affect the prototype chain

When we have a property that exists in multiple sub prototypes, javascript uses the one that is closest in the prototype chain. Suppose we want to change the behavior of the method toString in James so instead of using Object.prototype.toString, to James We add a method with the same name.

You can say that what we are doing is updating it.

James.toString = functions () {
  return `${this.name} ${this.edad}`;
};
James.toString(); // 'Jaime 33'

Now the method is used toString of James, the prototype chain is not traversed until reaching Object.prototype.toString as was done in the previous examples. Because there is already a method toString directly in our object James.

The operator delete does not remove properties from prototypes

When we occupy the operator delete, this never touches the prototype chain, it only eliminates the properties of the object in question.

If a property with the same name as the one we removed exists in the prototype chain, then now that property in the prototype chain will be used.

Using the previous example, of the method toString of James, if we eliminate it with delete, so now the toString() of Object.prototype It is the one that will be used because the person object does not have that method directly.

James.toString = functions () {
  return `${this.name} ${this.edad}`;
};
James.toString(); // 'Jaime 33'
delete James.toString
James.toString(); // '[[object Object]]' --> De Object.prototype

In the last line we notice that now James use the again toString() of Object.prototype.

Examples of predefined objects in Node.js and Javascript

As we have already mentioned in other publications, except for primitive types, everything else in javascript are objects. In future publications we will see more detail about type objects functions and array

How to create an object functions?

functions printPerson(person) {
  console.log(person.name);
  console.log(person.edad);
}

As the function printPerson It is an object, we can add properties to it without any problem.

printPerson.miPropiedad = 'Mi propiedad de una funcion';
console.log(imprimierPersona.miPropiedad); // 'Mi propiedad de una funcion

The functions are very useful, so versatile that with them we can generate new objects, but that topic does not belong to this publication.

How to create an object array?

The recommended way is to simply create them using square brackets [].

const vacio = [];
const numeros = [1, 2, 3, 4, 5, 6];
const animales = ['perro', 'gato', 'caballo'];
const conObjetos = [
  		{
          name: 'Jaime',
          edad: 33
        },
        functions printPerson(person) {
          console.log(person);
        }
	];

Since an array is an object, just like a function, you can also add properties to it.

const numeros = [1, 2, 3, 4, 5, 6];
numeros.miPropiedad = 'Mi propiedad de un arreglo';
console.log(numeros.miPropiedad); // 'Mi propiedad de un arreglo

He array is the clearest object to use as a data structure.

How to create an object date?

const fecha = new date();
console.log(fecha); // Fri May 28 2021 10:46:27 GMT-0500 (hora de verano central)

As you can imagine, we can also add properties to the date object because it is an object

Conclusion

In Javascript and on any platform where it is executed, an example is in node.js, they have the peculiarity of always having a link to a prototype. This is how the language is designed. I already know that classes currently exist, but in reality these are functions that internally make use of prototypes, we will also explain this in another publication.

With the exception of primitive data, everything else in Javascript is objects, it is important to know how they work so as not to get stuck in this learning.

With this information you have enough to continue advancing and use items more wisely.

The topic of objects is quite extensive, there are still many things to take into account, but there is already too much information that we have to divide it into more publications. So very soon we will continue. If you have any questions, do not hesitate to write them in the comments, we will be happy to help you!

¿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 and 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 in 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 and 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);

The property 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>

The property 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 and 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 in props

El contenido de nuestro saludo lo podemos agregar en el object 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?

functions 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í:

functions 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.

functions 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

References

https://reactjs.org/

https://babeljs.io/

Instalar multiples versiones de node.js con NVM en macOS, Linux y Windows

Instalar multiples versiones de node.js con NVM en macOS, Linux y Windows

Introduction

Si no sabes que es node y nvm, aquí te explicaré que son, para qué sirven y además como instalar node.js con NVM para tener múltiples versiones y ser más productivo a la hora de crear tus paquetes.

¿Qué es node.js?

Tomando la definición del sitio oficial, Node.js®, node es un entorno de ejecución para JavaScript construido con el motor de JavaScript V8 de Chrome. Node.js usa un modelo de operaciones E/S sin bloqueo y orientado a eventos, que lo hace liviano y eficiente. El ecosistema de paquetes de Node.js, npm, es el ecosistema más grande de librerías de código abierto en el mundo.

Vamos a describir la parte que nos interesa, Node.js es un programa, V8 es un motor de javascript de código abierto creado por Google, por lo que también lo hace un programa, V8 está escrito en C++, y la tarea de V8 es tomar código Javascript y convertirlo a código máquina (compilar código), pero lo que lo hace especial para nuestros fines es que puede ser embebido dentro de otros programas, lo que permite que V8 esté embebido en Node.js, V8 por así decirlo es el punto de partida para toda la funcionalidad de Node.js.

Node.js también está escrito en C++ y utiliza la API de V8 para agregarle características y funcionalidades nuevas a Javascript. Estas nuevas funcionalidades permiten tener acceso al sistema de archivos y carpetas, nos permite crear un servidor TCP y http, además de acceso a POSIX, o sea, a toda la funcionalidad del sistema operativo donde se encuentre instalado.

Node.js proporciona la sintaxis Javascript para crear programas que tengan acceso a las características del sistema operativo donde sé está ejecutando.

Cuando instalas node.js viene por default con una herramienta llamada NPM, que es el administrador de paquetes de node, con esta herramienta tú puedes instalar cualquier paquete de JavaScript que necesites.

¿Cómo lo hace? Pues cualquier desarrollador de javascript que siga los lineamientos adecuados puede publicar su librería/paquete, esta publicación genera un registro que internamente utiliza la línea de comandos de NPM para descargar e instalar ese nuevo paquete, además de que actualmente muchísimos paquetes ya registrados que puedes descargar y usar en tus proyectos.

¿Qué es NVM?

Para instalar node.js con NVM en macOS, primero debes tener git instalado para poder correr el comando de instalación de NVM. Ahora bien, ¿Qué es NVM? NVM significa Node Version Manager, y es al administrador de versiones de node, te permite tener múltiples versiones de node en tu computadora, dándote la flexibilidad de cambiar entre versiones y tener tu propio ambiente de trabajo en cada versión.

Puedes tener tú propios paquetes instalados, específicos para diferentes proyectos. Lo que tienes instalado en una versión, no choca con lo que tienes en otra.

Todo esto te da la habilidad de probar diferentes tipos de versiones de tus paquetes. Si estás creando un nuevo paquete y lo quieres probar en las últimas versiones de node.js, lo puedes hacer sin temor a romper lo que ya tienes para otra versión más estable o incluso para versiones más viejitas.

¿Cómo instalamos node.js con NVM en macOS y Linux?

Suponemos que ya tienes instalado git, si no es así, puedes ver como instalar git en macOS aqui.

Ejecuta el siguiente comando en una terminal:

curl -either- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
Instalar nvm en mac o linux
Instalar nvm en mac o linux

Este comando te clonara la última versión estable de NVM en tu directorio ~/.nvm, luego intentara escribir en tu archivo ~/.zshrc either ~/.bashrc el código de abajo. Esto para que NVM se cargue y funcione correctamente al iniciar sesión en tu computadora.

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

Ahora para ejecutar el código anterior, y nos funcioné desde este momento nvm, ponemos en la consola el siguiente comando. Esto te permite no cerrar y volver abrir tu shell.

source ~/.zshrc

Revisa si tu instalación de NVM quedo correcta, ejecutando:

nvm --version

Si te imprime la versión, es que todo quedo correctamente instalado.

Ahora instala la última versión estable de node.js con el comando nvm install version

nvm install node # Aqui "node" es una alias para instalar la ultima version

Y verifica la versión de node instalada con:

node --version

¿Como usar otras versiones de node.js con NVM en macOS y Linux?

Para listar todas las versiones de node.js, puedes ejecutar el siguiente comando:

nvm ls-remote

La terminal te mostrará algo así:

nvm ls-remote
nvm ls-remote

Si quieres instalar otra versión, vuelves a ejecutar el comando nvm install version, por ejemplo, vamos a instalar la versión 14:

nvm install 14

Este comando te instalará la versión 14 de node, pero para poder usarlo, debemos ejecutar el siguiente comando:

nvm use 14

Ahora que se instaló la version 14 de node, siempre que se instala una nueva versión, nvm toma como default esa versión recientemente instalada de node en la shell actual, donde se ejecutó el comando de instalación.

Para poder verificar la versión actual, puedes ejecutar el comando `nvm current`, si quieres ver todas las versiones instaladas ejecuta el comando `nvm ls`, el cual lista las versiones instaladas de node.js

nvm ls
nvm ls

¿Cómo instalar node.js con NVM en windows?

Para instalar node.js en windows existen dos opciones similares a NVM, no son lo mismo, pero cumplen con el objetivo de tener varias versiones de node.js para facilitar tu desarrollo de software.

  • NVS
  • NVM windows

Para usar estos comandos se recomienda hacerlo desde tu power shell de windows con derechos de administrador. También desde una terminal muy buena, y que la puedes descargar del siguiente enlace:

https://cmder.net/

Instalar estas dos herramientas en windows, es mucho más fácil porque tiene sus propios instaladores automáticos.

nvs windows

Primero hay que descargar el instalador del enlace de abajito. NVS de hecho fue creado con inspiracion de NVM y tambien se puede usar en Mac y Linux.

https://github.com/jasongin/nvs/releases

Instalador de NVS
Instalador de NVS

Ahora, la operación confiable de “siguiente”, “siguiente”, “siguiente”, “finalizar”. Una vez instalado, en tu terminal de preferencia ejecuta el comando nvs add [version].

nvs add 16

Y para usar una version especifica de node, simplemente en una terminal, ejecuta el comandp nvs use [version].

nvs use 16

nvm-windows

Para instalar nvm-windows es también muy fácil, solo descárgate el instalador de aquí.

https://github.com/coreybutler/nvm-windows/releases

Luego, ya sabes, la operación confiable de oprimir el botón next hasta que la instalación esté completa.

Los comandos que puedes utilizar son:

Para instalar una versión de node ocupamos el comando:

nvm install v16

Si tienes más de una versión de node, puedes elegir cuál usar, al igual que en linux y MacOS.`

nvm use v16

Listo ya sabes como instalar node.js en tu máquina, ahora sí, tienes todo lo necesario para empezar a desarrollar en javascript.

en_USEN