Ir al contenido
  1. Blog/

Cómo trabajar con React Context y Hooks

·7 mins·
Esta mañana, leía la fechas de lanzamientos de algunas librerías de desarrollo de software que hoy damos sentadas. React, por ejemplo, se lanzó el 29 de mayo del año 2013. En estos casi 10 años, hemos visto la evolución de este ecosistema. Hoy, te mostraré cómo hacer uso de una de las características más útiles de este popular Framework: el contexto.

Antes de empezar #

Estoy asumiendo que:

  • Haz trabajado con React.
  • Haz utilizado Hooks.
  • Haz trabajado con librerías como Redux.

Si no, no importa. Igual quédate, de una forma u otra debes dar el primer paso.

Ok, React Context #

In a nutshell, React Context nos permite compartir el State de nuestra aplicación a través del árbol de componentes sin tener que explicítamente enviar o recibir propiedades entre sí.

Veamos el siguiente ejemplo en el archivo App.js, un carrito de compras:

 1import React from "react";
 2import { Container, Row, Col, Form } from "reactstrap";
 3
 4export default function App() {
 5  const [cart] = React.useState([{ name: `iPad` }, { name: `OnePlus 9` }]);
 6  const [user] = React.useState({ name: `Mario` });
 7  /* En nuestra App, aquí tenemos el origen de datos */
 8  return <Layout cart={cart} user={user} />;
 9}
10
11function Layout({ cart, user }) {
12  /* Construímos el esqueleto de la interfaz */
13  return (
14    <Container>
15      <Row>
16        <Col>
17          <CartForm cart={cart} user={user} />
18        </Col>
19      </Row>
20    </Container>
21  );
22}
23
24function CartForm({ cart, user }) {
25  /* Aquí consumimos a user, pero no a cart */
26  return (
27    <Form>
28      <h1>Carrito de {user.name}</h1>
29      <CartList cart={cart} />
30      <button type="submit">Buy</button>
31    </Form>
32  );
33}
34
35function CartList({ cart }) {
36  /* Finalmente consumimos a cart */
37  return (
38    <div className={`p-2`}>
39      {cart.map((item, index) => (
40        <div key={index}>
41          <span style={{ color: `red`, cursor: `pointer` }}>[x]</span>
42          <span className={`mx-1`} />
43          <span>{item.name}</span>
44        </div>
45      ))}
46    </div>
47  );
48}

Seguro notaste como los States user y cart tienen que enviados y recibidos por todos los componentes del árbol para llegar a esos dónde realmente serán utilizados. Creéme cuando te digo que esto puede complicarse aún más.

Sería de mucha ayuda poder acceder a ellos justo dónde los necesitemos.

Provider, Consumer, useReducer y useContext #

Ya que el título de esta sección me dio tu atención, vamos a introducir unos cuantos conceptos:

  • Provider: Como su nombre lo sugiere (proveedor en español), es el componente que proveerá los datos a todos sus componentes hijos. Es aquí dónde el State vivirá.

  • Consumer: Con él, cada nodo (o componente) puede acceder al State que vive en el Provider.

Estos dos conceptos son fundamentales para entender lo qué sucede al implementar React Context en tu aplicación. Sin embargo, el título de este artículo tiene un “y Hooks” al final. Así que también debemos explicar lo siguiente:

  • useReducer: Si has utilizado Redux, ya conoces el próposito de un reducer. Es una función que recibe dos parametros, el state actual y una action. Con estos dos párametros, podemos organizar la forma en cómo el State será actualizado reduciendo las actualizaciones a casos. Los utilizaremos aquí junto con useContext.

  • useContext: In a nutshell, este Hook actúa como un Consumer.

Ah sí, el Context #

Lo siguiente en nuestra lista es:

  • Configurar un Contexto
  • Establecer una forma de actualizar el Context
  • Consumir el Contexto

Para eso, necesitamos el siguiente script, context.js. Encontrarás comentarios explícativos.

 1import React from "react";
 2
 3const StateContext = React.createContext();
 4const DispatchContext = React.createContext();
 5
 6function reducer(state, action) {
 7  switch (action.type) {
 8    case `REMOVE_ITEM`:
 9      const index = Number(action.data.index);
10      return {
11        ...state,
12        cart: [...state.cart.slice(0, index), ...state.cart.slice(index + 1)],
13      };
14
15    default:
16      return state;
17  }
18}
19
20function Provider({ children }) {
21  /*
22   * Creamos un State usando el hook useReducer
23   * De esta manera, obtenemos la habilidad de separar nuestra lógica
24   * En acciones.
25   *
26   * Ver: https://es.reactjs.org/docs/hooks-reference.html#usereducer
27   */
28  const [state, dispatch] = React.useReducer(reducer, {
29    user: { name: `Mario` },
30    cart: [
31      { name: `iPad Air` },
32      { name: `OnePlus 9` },
33      { name: `Thinpad X1 Carbo 9 Gen` },
34    ],
35  });
36
37  /*
38   * Establecemos 2 Providers
39   * 1 para proveer el State
40   * 1 para proveer la función Dispatch
41   *
42   * Esto es así, ya que la función Dispatch nunca cambiará
43   * Por esta razón, la separamos del resto del State
44   */
45  return (
46    <StateContext.Provider value={state}>
47      <DispatchContext.Provider value={dispatch}>
48        {children}
49      </DispatchContext.Provider>
50    </StateContext.Provider>
51  );
52}
53
54function useConsumer() {
55  /*
56   * Finalmente, hacemos uso del Hook useContext
57   * para consumir el State que reside en cada Provider
58   *
59   * La razón por la que es un Array es para darle el formato
60   * de un custom Hook.
61   */
62  return [
63    React.useContext(StateContext),
64    React.useContext(DispatchContext),
65  ].map((ctx) => {
66    if (ctx === undefined) throw new Error(`Provider not found`);
67    return ctx;
68  });
69}
70
71/* Exportamos */
72export { Provider, useConsumer };

En el script anterior, nos encontramos con 3 funciones importantes.

  • Provider: Más que una función, un tipo. Para ser más específicos, un componente de React. Este componente proveerá el State global.

  • useConsumer: Más que una función, un custom Hook. Con él, consumiremos el State en los hijos del componente <Provider />.

  • reducer: Nuestra función reducer. Con ella, organizaremos cómo actualizaremos nuestro State.

Por último, exportamos únicamente lo que usaremos en los componentes dónde necesitemos el State. En nuestro script App.js.

24function CartForm({ cart, user }) {
25  /* Aquí consumimos a user, pero no a cart */
26  return (
27    <Form>
28      <h1>Carrito de {user.name}</h1>
29      <CartList cart={cart} />
30      <button type="submit">Buy</button>
31    </Form>
32  );
33}
24function CartForm()) {
25
26  const [state, dispatch] = useConsumer();
27
28  function removeItem(index) {
29    dispatch({ type: `REMOVE_ITEM`, data: { index } });
30  }
31
32  /* Aquí consumimos a user, pero no a cart */
33  return (
34    <Form>
35      <h1>Carrito de {state.user.name}</h1>
36      <CartList cart={state.cart} removeItem={removeItem} />
37      <Button type="submit" onClick={() => alert(`Thank you!`)}>Buy</Button>
38    </Form>
39  );
40}

En este ejemplo, <CartForm> es el componente desde el cuál accedimos al contexto utilizando nuestro Consumer useConsumer. En él, programamos lógica para poder ejecutar acciones como remover un item de la lista del carrito. En el archivo App.js:

42function CartList({ cart }) {
43  /* Finalmente consumimos a cart */
44  return (
45    <div className={`p-2`}>
46      {cart.map((item, index) => (
47        <div key={index}>
48          <span style={{ color: `red`, cursor: `pointer` }}>[x]</span>
49          <span className={`mx-1`} />
42function CartList({ cart, removeItem }) {
43  /* Finalmente consumimos a cart */
44  return (
45    <div className={`p-2`}>
46      {cart.map((item, index) => (
47        <div key={index}>
48          <span style={{ color: `red`, cursor: `pointer` }} onClick={() => removeItem(index)}>[x]</span>
49          <span className={`mx-1`} />

Te preguntarás, ¿Por qué consumir el State únicamente en el componente <CartForm /> y no también en <CartList>? La respuesta es simple. El React Context es una herramienta muy poderosa, pero no significa que debamos usarla para todo.

En nuestro ejemplo, es más importante mantener el componente <CartList> reusable, de esta forma, si tenemos que renderizar la lista en otro lado, no dependeremos del contexto que creamos aquí.

No hay una solución milagrosa #

Espero te haya sido de utilidad este Post. Seguramente verás como más y más proyectos utilizan las APIs nativas de React para manejar varibles globales, en lugar de instalar librerías de terceros.

Únicamente ten en cuenta que estás son meramente herramientas, y no hay una sola que sea perfecta para cada caso de uso. En el ejemplo, tomamos la decisión de dejar el componente presentacional <CartList> independiente para poder usarlo en otros contextos sin problemas.

Referencias #