Cómo crear un buscador de criptomonedas con Redux, parte 1

Cómo 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:

<img src="https://firebasestorage.googleapis.com/v0/b/pensemosweb-mx.appspot.com/o/posts%2Ferror-falta-mapeo.webp?alt=media&token=7c861e3c-5050-4b38-821e-776cc2f5ebba" alt="Error, Falta de Mapeo" title="Error de falta de mapeo" loading="lazy" decoding=""async" /> 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:

<img src="https://firebasestorage.googleapis.com/v0/b/pensemosweb-mx.appspot.com/o/posts%2Fresultado-final-parte-1.webp?alt="media&token=03458174-5c12-4d09-a65a-77957aec1257" alt="Resultado final parte 1" title="Resultado final" loading="lazy" decoding="async" />

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.

Podría interesarte

¿Cómo crear un servidor web en 5 minutos con Node.js?

Para crear un servidor web en node.js, primero, ¿Qué es node.js?, tomando la definición del sitio oficial, 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.