Cuando comencemos a crear componentes en nuestra aplicación React, nos encontraremos con la necesidad de «compartir datos» entre unos y otros, es decir, enviar datos de un componente padre a otro componente hijo. Recordemos que React tiene varias normas:
- ✨ Los datos que llegan desde un componente padre, son las props. ❌ No pueden alterarse.
- ✨ Los datos que cambian en un componente, se denominan estado. ✅ Pueden alterarse.
- ⛔ No está permitido enviar datos desde un componente hijo a su componente padre.
Si queremos que un componente padre comparta un estado con su elemento hijo, lo más sencillo es enviarselo por props. Pero, ¿qué ocurre si queremos hacerlo desde un componente padre a su componente nieto (o aún un descendiente mayor)? ¿Qué ocurre si queremos enviar datos desde un componente hijo a su padre?
¿Qué es el Prop Drilling?
Como hemos mencionado, la forma más sencilla de enviar datos de un componente padre a un componente hijo es utilizar las props. Esto es algo muy sencillo y práctico cuando hablamos de componentes directos, es decir, que son inmediatos entre ellos.
Sin embargo, si necesitamos enviar a través de varios componentes, como por ejemplo main -> header -> counter
(o jerarquías incluso más largas) donde los únicos implicados son sus extremos y los componentes intermedios simplemente son intermediarios, este concepto se denomina Prop Drilling:
El Prop Drilling es una técnica válida cuando lo realizamos una vez o cuando no hay muchos niveles entre componentes, pero cuando no es así, el código se puede volver muy repetitivo y dificultar la legibilidad de código, ya que no están haciendo nada más que pasar y enviar los datos de uno a otro. Además, también puede ser muy fragil a la hora de mantener dicho código.
Imaginemos que la lógica de nuestro contador la vamos a utilizar tanto en App
como en Counter
. Por esta razón, vamos a definirla en App
y para poder usarla en Counter
la vamos pasando por props:
function App() {
const [counter, setCounter] = useState(0);
return (
<div>
<Main counter={counter} />
</div>
);
}
function Main({ counter }) {
/* ... */
return <Header counter={counter} />;
}
function Header({ counter }) {
/* ... */
return <Counter counter={counter} />;
}
function Counter({ counter }) {
return <h1>Este es el valor del contador: {counter}</h1>;
}
Como puedes ver, si los datos pasan por múltiples componentes (caso que suele ser muy habitual) se vuelve un proceso muy tedioso de escribir y de leer. Por esta razón, quizás habría que analizar y valorar alternativas al Prop Drilling que permitan el paso de datos de un componente a otro sin ensuciar tanto el código.
La API de Contexto
Una forma ideal para compartir ese estado entre componentes de diferentes niveles y no resulte tan tediosa es utilizar la API de Contexto de React. Esta API nos permite crear un contenedor de contexto que insertaremos en nuestro JSX y envolverá a todos los componentes con los que queramos compartir el estado.
Luego, mediante el hook useContext
podremos acceder a ese estado compartido en cualquiera de los elementos hijos que envuelva ese contenedor de contexto. Veamos un ejemplo paso a paso.
Lo primero sería crear el contexto en una estructura de carpetas dedicada. De este modo, no estará acoplado al código y será fácil de ampliar y mantener. Por ejemplo, al igual que tenemos una carpeta src/components/
, vamos a crear una carpeta src/contexts/
:
src/
|---- components/
|--- Main.jsx
|--- Header.jsx
|--- Counter.jsx
|---- contexts/
|--- CounterContext.js
|---- App.jsx
Ahora, en ese archivo CounterContext.js
se va a incluir la lógica del contexto de nuestro contador. Por lo tanto, importamos useState
para crear nuestro estado y createContext
para crear nuestro contexto.
Este fichero tendrá dos exportaciones:
- 1️⃣
CounterProvider
que es el contenedor padre que proveerá los datos - 2️⃣
CounterContext
que lo usaremos en los hijos con el hookuseContext
para obtener los datos
import { createContext, useState } from 'react';
export const CounterContext = createContext();
export const CounterProvider = ({ children }) => {
const [counter, setCounter] = useState(0);
return (
<CounterContext.Provider value={{ counter, setCounter }}>
{children}
</CounterContext.Provider>
);
};
La función CounterProvider
utiliza el CounterContext.Provider
como contenedor padre y en la prop value
añade todos los datos a compartir, en este caso un objeto con counter
y setCounter
. Observa también que utilizamos children
para que nuestra función de contexto sea reutilizable facilmente:
import { CounterProvider } from "../contexts/CounterContext.js";
import Main from "./Main.jsx";
export function App() {
return (
<CounterProvider>
<Main />
</CounterProvider>
);
}
Por otro lado, a la hora de usarlo en los elementos hijos, simplemente importamos el contexto y lo utilizamos con el hook useContext
. Luego, simplemente utilizamos los datos donde corresponda:
import { useContext } from "react";
import { CounterContext } from "../contexts/CounterContext.js";
export function Counter() {
const { counter, setCounter } = useContext(CounterContext);
return <h1>El valor del contador es: {counter}</h1>;
}
En este caso estamos utilizando counter
sólo, por lo que setCounter
se podría omitir. Sin embargo se ha añadido para que se vea más fácilmente en el ejemplo como pasar múltiples valores.