Como crear un buscador de criptomonedas con Redux, parte  1

Como crear un buscador de criptomonedas con Redux, parte 1

Introducci贸n

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

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

驴Qu茅 queremos lograr?

Buscador parte 1 terminada
Buscador parte 1 terminada

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

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

https://b8lxc.csb.app/

Primera iteraci贸n, datos en duro en los componentes

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

驴C贸mo son los datos de la respuesta del API?

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

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

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

Componente CryptoMarkets

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

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

Componente SearchField

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

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

Componente Table

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

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

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

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

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

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

Componente TableRow

/src/CryptoMarkets/TableRow.js

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

Usando datos dummies.js

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

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

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

Generando filas din谩micamente y dummies separados.

Primero separamos los dummies y les damos forma.

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

Luego utilizamos esos dummies en instancias del componente TableRow.

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

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

Buscador con dummies
Buscador con dummies

Segunda Iteraci贸n, agregando Redux

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

En tu computadora local corre el siguiente comando.

yarn add redux react-redux

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

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

Configurando inicialmente el store

Crearemos un archivo nuevo /configureStore.js.

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

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

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

Hacer al store accesible

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

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

useContext y useReducer, 驴C贸mo replicar redux?

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

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

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

Acceder al store desde el componente contenedor CryptoMarkets

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

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

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

Componente Table recibe headers y rows como propiedades

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

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

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

Buscador con dummies
Buscador con dummies

Implementar la busqueda de mercados

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

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

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

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

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

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

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

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

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

Disparar acci贸n de filtrado desde el componente CryptoMarkets

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

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

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

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

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

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

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

Buscador implementado
Buscador implementado

Refactor, action creator para disparar b煤squeda

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

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

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

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

Tercera iteraci贸n, obteniendo datos de la API simulada

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

Respuesta json de bitso

Simplemente en tu navegador pega la siguiente url:

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

Mercados de bitso
Mercados de bitso

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

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

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

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

Usar redux-thunk para obtener los markets

驴Qu茅 es un thunk?

Su definici贸n en el 谩mbito de programaci贸n en general:

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

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

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

Crear un Thunk action creator

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

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

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

Actualizar reducer para entender la acci贸n RECEIVE_MARKETS

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

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

Hacer que el store entienda las funciones thunk

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

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

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

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

Invocando el thunk desde el componente CryptoMarkets

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

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

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

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

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

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

El error debe mostrarse asi:

Error, falta de mapeo
Error falta de mapeo

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

Mapeando datos a UI

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

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

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

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

Y modificamos nuestro thunk de la siguiente forma.

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

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

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

Resultado final parte 1
Error arreglado

Estilos en campo de b煤squeda

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

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

En el componente importamos y aplicamos estilos:

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

Ahora si tenemos el resultado final:

Buscador parte 1 terminada
Buscador parte 1 terminada

Conclusi贸n. M谩s iteraciones

Todav铆a quedan m谩s cosas por hacer y mejorar, al igual que hicimos en esta publicaci贸n incrementando en iteraciones peque帽as hasta conseguir que funcione el buscador, as铆 tendremos m谩s publicaciones para continuar mejorando e incrementando a煤n m谩s las funcionalidades.

useContext y useReducer. 驴C贸mo replicar redux?

useContext y useReducer. 驴C贸mo replicar redux?

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

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

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

Contexto

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

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

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

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

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

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

createContext(defaultContext)

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

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

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

Ahora en nuestro archivo App.js importamos el contexto.

import UserNameContext from './UserNameContext';

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

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

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

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

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

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

static contextType = UnContexto

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

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

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

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

<UnContexto.Consumer>

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

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

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

useContext(UnContexto)

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

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

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

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

Aqu铆 puedes ver el ejemplo completo:

Edit useContext(UserNameContext)

useReducer

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

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

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

Sintaxis

La sintaxis es la siguiente

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

Ejemplo

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

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

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

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

initialState e initiLazyFn

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

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

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

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

Funci贸n reducer

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

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

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

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

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

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

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

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

Edit useReducer(reducer, initState, initLazyFn)

Preparando codigo para combinar useContext y useReducer

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

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

actionTypes.js

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

actions.js

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

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

Funci贸n reducerUser

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

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

Uniendo todo en App.js

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

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

Mini redux con useContext y useReducer

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

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

Header y UserInfo

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

Archivo /components/Header.js.

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

Archivo /components/UserInfo.js

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

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

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

Crear contexto

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

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

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

Uso de useContext y useReducer para crear el store

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

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

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

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

Usando useContext para usar store en UserForm

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

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

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

Agregar <UserForm>

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

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

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

Aqu铆 puedes ver el ejemplo completo.

Edit useContext y useReducer, replicando Redux

Conclusiones

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

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

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

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

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

Referencias

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

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

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

驴C贸mo empezar con Redux en 7 minutos?

驴C贸mo empezar con Redux en 7 minutos?

驴Qu茅 es Redux?

Redux es un contenedor predecible del estado de aplicaciones JavaScript.

Te ayuda a escribir aplicaciones que se comportan de manera consistente, corren en distintos ambientes (cliente, servidor y nativo), y son f谩ciles de probar. Adem谩s de eso, provee una gran experiencia de desarrollo, gracias a edici贸n en vivo combinado con un depurador sobre una l铆nea de tiempo.

redux.org

Te permite controlar el flujo de datos de una aplicaci贸n Javascript, este flujo de los datos funciona en una sola direcci贸n. Por esta 煤nica direcci贸n es mucho m谩s f谩cil controlar las mutaciones y operaciones as铆ncronas.

Redux es parecido a flux, de hecho est谩 basado en flux

En el estado de una aplicaci贸n, principalmente en aplicaciones SPA, esto es muy importante, porque las aplicaciones web modernas tienen gran complejidad de operaciones as铆ncronas. Adem谩s de controlar el estado entre los componentes.

驴Qu茅 problemas puedo mitigar con Redux?

En el ejemplo de aplicaciones web, estas necesitan controlar peticiones a servidores, obtener datos y controlar el estado en el cliente. Aun cuando estos datos no han sido guardados en el servidor.

Estas aplicaciones modernas requieren de un control complejo y por eso nuevos patrones de arquitectura como Redux y flux  nacen para hacer el desarrollo m谩s productivo.

Las mutaciones de objetos son dif铆ciles de manejar y m谩s a escalas medianas y grandes. Se comienza a perder el control de los datos, este flujo de datos generan comportamiento no deseados. Mala informaci贸n desplegada al usuario y c贸digo que es muy dif铆cil de mantener.

Sin contar cosas m谩s elevadas como tu salud y causa de estr茅s por el esfuerzo en arreglar estas inconsistencias, te hace perder tiempo para mantener la aplicaci贸n funcionando correctamente. Por si fuera poco afectas a tus usuarios porque ellos obtienen informaci贸n incorrecta y pierden su tiempo al utilizar esa informaci贸n.

驴Qu茅 decir de las operaciones as铆ncronas? Bueno, pr谩cticamente pasa lo mismo o a煤n peor. Porque en una operaci贸n as铆ncrona no sabes cuando obtendr谩s el resultado y adem谩s normalmente vienen acompa帽adas con el deseo de hacer modificaciones al estado de la aplicaci贸n.

Un ejemplo com煤n es el control de los datos de tu aplicaci贸n con cualquier frawework front-end de componentes visuales.

驴De qu茅 est谩 hecho Redux? Tres elementos

Store. El store es un objeto donde guardas toda la informaci贸n del estado, es como el modelo de una aplicaci贸n, con la excepci贸n que no lo puedes modificar directamente, es necesario disparar una acci贸n para modificar el estado.

Actions. Son el medio por el cual indicas que quieres realizar una modificaci贸n en el estado, es un mensaje o notificaci贸n liviana. Solo enviando la informaci贸n necesaria para realizar el cambio.

Reducers. Son las funciones que realizan el cambio en el estado o store, lo que hacen internamente es crear un nuevo estado con la informaci贸n actualizada, de tal manera que los cambios se reflejan inmediatamente en la aplicaci贸n. Los reducers son funciones puras, es decir, sin efectos colaterales, no mutan el estado, sino que crean uno con informaci贸n nueva.

驴Qu茅 principios debo seguir? Tres principios

Un 煤nico Store para representar el estado de toda la aplicaci贸n. Tener una sola fuente de datos para toda tu aplicaci贸n permite tener centralizada la informaci贸n, evita problemas de comunicaci贸n entre componentes para desplegar los datos, f谩cil de depurar y menos tiempo agregando funcionalidad o detectando errores.

Estado de solo lectura. Esto permite tener el control de cambios y evitar un relajo entre los diferentes componentes de tu aplicaci贸n, ni los componentes, ni peticiones ajax pueden modificar directamente el Estado (state) de tu aplicaci贸n, esto quiere decir que si quieres actualizar tu estado, debes  hacerlo a trav茅s de actions, de esta manera redux se encarga de realizar las actualizaciones de manera estricta y en el orden que le corresponden.

Los cambios solo se hacen con funciones puras. Al realizar los cambios con funciones puras, lo que realmente se hace es crear un nuevo objeto con la informaci贸n actualizada, estas funciones puras son los reducers y no mutan el estado, al no mutar el estado, se evitan problemas de control, datos incorrectos, mal comportamiento y errores, tambi茅n permite que la depuraci贸n sea m谩s f谩cil. Puedes dividir los reducer en varios archivos diferentes y las pruebas unitarias son f谩ciles de implementar. Los reducers son funciones puras que toman el estado anterior y una acci贸n, y devuelven un nuevo estado.

Otras arquitecturas como MVC (Modelo, Vista, Controlador), los cambios pueden existir en ambas direcciones, es decir, la vista puede cambiar el estado, el modelo lo podr铆a modificar y tambi茅n el controlador. Todos estos cambios necesitan estar sincronizados en varias partes de una aplicaci贸n para evitar inconsistencias, lamentablemente este problema de inconsistencia se vuelve muy dif铆cil y tedioso de resolver.

Lo anterior no sucede con Redux.

驴C贸mo funciona? Mi primer estado predecible con Redux

Primero veamos el flujo de una sola direcci贸n de redux:

Redux: flujo de control una sola direcci贸n
Redux: flujo de control una sola direcci贸n

Ahora un ejemplo concreto para saber de que estamos hablando, el control de un contador:

See the Pen Redux: Contador State, es5 by Jaime Cervantes Velasco (@jaime_cervantes_ve) on CodePen.

Si quieres probar este ejemplo en tu m谩quina recuerda insertar o importar la librer铆a Redux. En este ejemplo podemos ver el funcionamiento de Redux, cada vez que se da clic sobre los botones de incrementar y decrementar, el contador se incrementa y decrementa.

Si ahora nos vamos al c贸digo JS, podremos ver que solo tenemos una funci贸n llamada counter, esta funci贸n es un reducer y es una funci贸n pura, sin efectos colaterales.

Luego vemos que como par谩metros recibe un state y un action, cuando creamos nuestro Store con Redux, estas funciones reducer son utilizadas para modificar el State.

Normalmente, el state, reducer y action son definidos en puntos diferentes, y pueden ser organizados en varios archivos y carpetas dependiendo de la necesidad de la aplicaci贸n, toda esta organizaci贸n tiene la finalidad de tener un c贸digo limpio y separar funcionalidades. En este caso sencillo no es necesario, en un solo archivo tenemos todo.

State

Nuestro state es solo un valor num茅rico, el cual se va a incrementar o decrementar con los botones que est谩n en la p谩gina, normalmente el state es un objeto literal o un array literal, pero en nuestro caso solo necesitamos un n煤mero. Su definici贸n podr铆a estar en otro archivo, sin embargo, para este ejemplo no es necesario, lo agregamos como valor por defecto del par谩metro state.

function counter(state = 0, action) { ...

Actions

Si seguimos revisando nuestro c贸digo, veremos unos cuantas condiciones que revisa el valor del par谩metro action y dependiendo de la acci贸n, se ejecuta alguna operaci贸n, en nuestro caso son las operaciones INCREMENTAR o DECREMENTAR.

Los actions no son m谩s que objetos literales como los siguientes:

const incrementar = { type: 'INCREMENTAR' }
const decrementar = { type: 'DECREMENTAR' }

Reducers

Los reducer revisan esos actions, en nuestro ejemplo:

if (action.type === 'INCREMENTAR') {
  return state +  1;
}

if (action.type === 'DECREMENTAR') {
  return state - 1;
}

Con todo esto ya tenemos nuestro State y su estado inicial en caso de no estar definido, tambi茅n nuestra primer reducer, counter, y nuestros primeros actions INCREMENTAR y DECREMENTAR.

Store

Es el momento de crear nuestro Store, utilizando la librer铆a redux esto es muy f谩cil:

const store = Redux.createStore(counter);

Con la anterior l铆nea, Redux crea un Store para controlar el estado de nuestra aplicaci贸n. Internamente, un Store es un Patr贸n observador que utiliza un Singleton para el State y expone los siguientes m茅todos principales:

  • store.getState()
  • store.subscribe(listener)
  • store.dispatch(action)

store.getState() te permite obtener el estado actual de tu aplicaci贸n.

store.subscribe(listener) ejecuta la funci贸n listener (u observador), cada vez que el store es actualizado.

store.dispatch(action) pide actualizar el estado, esta modificaci贸n no es directa, siempre se realiza a trav茅s de un action y se ejecuta con un reducer.

Reaccionar a cambios del state

Luego creamos una funci贸n render, para que se ejecute cada vez que el State de tu aplicaci贸n cambie.

Aqu铆 nuestro contenido html:

<button id="incrementar">Incrementar</button>
<button id="decrementar">Decrementar</button>
<div id="state"></div>

Nuestra funci贸n render

function render () {
  document.querySelector('#state').innerText = store.getState();
}

Invocamos render para pintar el valor 0

render()

Aqu铆 te preguntar谩s, 驴C贸mo es posible que esa l铆nea imprima 0 en la pantalla?, pues internamente el store invoca un dispatch con un action vac铆o store.dispatch({}), esto invoca nuestra funci贸n reducer y al no encontrar ninguna acci贸n, entonces regresa el estado inicial 0.

Luego nos subscribimos a store para escuchar u observar el State cada vez que se actualice y poder ejecutar la funci贸n render().

store.subscribe(render);

Esta l铆nea permite que cuando demos clic en los botones de incrementar y decrementar, se imprima el nuevo valor en la p谩gina volviendo a renderizar su contenido.

Ejecutar dispatch a trav茅s de eventos del DOM

Y por 煤ltimo agregamos dos listeners correspondientes a los botones de incrementar y decrementar, cada bot贸n realiza una invocaci贸n dispatch para modificar el estado.

<button id="incrementar">Incrementar</button>
<button id="decrementar">Decrementar</button>
<div id="state"></div>
document.querySelector('#incrementar').addEventListener('click', () => {
  store.dispatch(incrementar);
});

document.querySelector('#decrementar').addEventListener('click', () => {
  store.dispatch(decrementar);
});

Al fin tenemos el flujo de datos de nuestro sencillo ejemplo usando Redux, en general los componentes que forman a una aplicaci贸n que utiliza Redux son los siguientes:

Redux: flujo de control completo
Redux: flujo de control completo

Conclusiones

Podemos notar el flujo en una sola direcci贸n desde la vista con los actions dispatch(action), luego  los reducers counter(prevState, action) para modificar el store y este 煤ltimo manda la informaci贸n a trav茅s de subscribe(render) y getState().

Redux como ya se mencion贸 en p谩rrafos anteriores, nos proporciona:

  • El conocimiento en todo momento del estado de nuestra aplicaci贸n y en cualquier parte de la aplicaci贸n con mucha facilidad.
  • F谩cil organizaci贸n, con actions, reducer y store, haciendo cambios en una sola direcci贸n, adem谩s puedes separar tus actions, reducer y el store en varios archivos.
  • F谩cil de mantener, debido a su organizaci贸n y su 煤nico store, mantener su funcionamiento y agregar nuevas funcionalidades es muy sencillo.
  • Tiene muy buena documentaci贸n, su comunidad es grande y tiene su propia herramienta para debuguear
  • Todos  los puntos anteriores hacen que sea f谩cil de hacer pruebas unitarias.
  • Y lo m谩s importante, te ahorra tiempo, dinero y esfuerzo xD.
es_MXES_MX