Pasar datos en profundidad con contexto
Usualmente, tú pasarías información desde un componente papá a un componente hijo por medio de props. Sin embargo, pasar props puede convertirse en una tarea verbosa e inconveniente si tienes que pasarlos a través de múltiples componentes, o si varios componentes en tu app necesitan la misma información. El contexto permite que cierta información del componente papá esté disponible en cualquier componente del árbol que esté por debajo de él sin importar qué tan profundo sea y sin pasar la información explícitamente por medio de props.
Aprenderás
- Qué es «perforación de props»
- Cómo reemplazar el paso repetitivo de props con contexto
- Casos de uso comunes para el contexto
- Alternativas comunes al contexto
El problema con pasar props
Pasar props es una gran manera de enviar explícitamente datos a través del árbol de la UI a componentes que los usen.
No obstante, pasar props puede conversirse en una tarea verbosa e incoveniente cuando necesitas enviar algunas props profundamente a través del árbol, o si múltiples componentes necesitan de las mismas. El ancestro común más cercano podría estar muy alejado de los componentes que necesitan los datos, y elevar el estado tan alto puede ocasionar la situación llamada «perforación de props».
¿No sería grandioso si existiese alguna forma de «teletransportar» datos a componentes en el árbol que lo necesiten sin tener que pasar props? ¡Con el contexto de React es posible!
Contexto: Una alternativo a pasar props
El contexto permite que el componente papá provea datos al árbol entero debajo de él. Hay muchas utilidades para el contexto. Este es un solo ejemplo. Considera el componente Heading
que acepta level
como su tamaño:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Heading level={2}>Heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={5}>Sub-sub-sub-heading</Heading> <Heading level={6}>Sub-sub-sub-sub-heading</Heading> </Section> ); }
Supongamos que tú quieres múltiples encabezados (headings) dentro del mismo componente Section
para siempre tener el mismo tamaño:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Section> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Section> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Section> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Actualmente, estás pasando la prop level
a cada <Heading>
separadamente:
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
Sería genial si tú pudieras pasar la prop level
al componente <Section>
y removerlo del <Heading>
. De esta forma podrías reforzar que todos los encabezados tengan el mismo tamaño en una misma sección (section):
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
¿Pero como podría el componente <Heading>
conocer el level
de su <Section>
más cercano? Eso requeriría alguna forma en la que el hijo «pediría» datos desde algún lugar arriba en el árbol.
No podrías lograrlo únicamente con props. Aquí es donde el contexto entra a jugar. Lo conseguirás en tres pasos:
- Crear un contexto. (puedes llamarlo
LevelContext
, ya que es para ellevel
de los encabezados) - Usar ese contexto desde el componente que necesita los datos. (
Heading
usaráLevelContext
) - Proveer ese contexto desde el componente que especifica los datos. (
Section
proveeráLevelContext
)
El contexto permite que en un papá (incluso uno distante) provea algunos datos a la totalidad del árbol dentro de él.
Paso 1: Crear el contexto
Primeramente, necesitas crear el contexto. Necesitarás exportarlo desde un archivo para que tus componentes lo puedan usar:
import { createContext } from 'react'; export const LevelContext = createContext(1);
El único parámetro que se le pasa a createContext
es el valor predeterminado. En este caso, 1
se refiere al nivel de encabezado más grande, pero tú puedes pasar cualquier valor (incluso un objeto). Ya verás la importancia del valor predeterminado en el siguiente paso.
Paso 2: Usar el contexto
Importa el Hook useContext
desde React y tu contexto:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
Actualmente, el componente Heading
lee level
con props:
export default function Heading({ level, children }) {
// ...
}
En su lugar, remueva la prop level
y lee el valor desde el contexto que acabas de importar, LevelContext
:
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
es un Hook. Así como useState
y useReducer
, únicamente puedes llamar a un Hook inmediatamente adentro de un componente de React (no dentro de ciclos o condiciones). useContext
le dice a React que el componente Heading
quiere leer el contexto LevelContext
.
Ahora que el componente Heading
no tiene una prop level
, ya no tienes que pasarla a Heading
en tu JSX de esta forma:
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
Actualiza el JSX para que sea Section
el que recibe la prop:
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Como recordatorio, esta es la estructura que estabas intentando que funcionara:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Nota que este ejemplo no funciona, ¡Aún! Todos los encabezados tienen el mismo tamaño porque pese a que estás usando el contexto, no lo has proveído aún. ¡React no sabe dónde obtenerlo!
Si no provees el contexto, React usará el valor predeterminado que especificaste en el paso previo. En este ejemplo, especificaste 1
como el parámetro de createContext
, entonces useContext(LevelContext)
retorna 1
, ajustando todos los encabezados a <h1>
. Arreglemos este problema haciendo que cada Section
provea su propio contexto.
Paso 3: Proveer el contexto
El componente Section
actualmente renderiza sus hijos:
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
Envuélvelos con un proveedor de contexto para proveer LevelContext
a ellos:
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
Esto le dice a React: «si cualquier componente adentro de este <Section>
pregunta por LevelContext
, envíales este level
.» El componente usará el valor del <LevelContext.Provider>
más cercano en el árbol de la UI encima de él.
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section level={1}> <Heading>Title</Heading> <Section level={2}> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section level={3}> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section level={4}> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
¡Es el mismo resultado del código original, pero no tuviste que pasar la prop level
a cada componente Heading
! En su lugar, «comprende» su nivel de encabezado al preguntar al Section
más cercano de arriba:
- Tú pasas la prop
level
al<Section>
. Section
envuelve a sus hijos con<LevelContext.Provider value={level}>
.Heading
pregunta el valor más cercano de arriba deLevelContext
por medio deuseContext(LevelContext)
.
Using and providing context from the same component
Currently, you still have to specify each section’s level
manually:
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Since context lets you read information from a component above, each Section
could read the level
from the Section
above, and pass level + 1
down automatically. Here is how you could do it:
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext.Provider value={level + 1}>
{children}
</LevelContext.Provider>
</section>
);
}
With this change, you don’t need to pass the level
prop either to the <Section>
or to the <Heading>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function Page() { return ( <Section> <Heading>Title</Heading> <Section> <Heading>Heading</Heading> <Heading>Heading</Heading> <Heading>Heading</Heading> <Section> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Heading>Sub-heading</Heading> <Section> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> <Heading>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ); }
Now both Heading
and Section
read the LevelContext
to figure out how «deep» they are. And the Section
wraps its children into the LevelContext
to specify that anything inside of it is at a «deeper» level.
Context passes through intermediate components
You can insert as many components as you like between the component that provides context and the one that uses it. This includes both built-in components like <div>
and components you might build yourself.
In this example, the same Post
component (with a dashed border) is rendered at two different nesting levels. Notice that the <Heading>
inside of it gets its level automatically from the closest <Section>
:
import Heading from './Heading.js'; import Section from './Section.js'; export default function ProfilePage() { return ( <Section> <Heading>My Profile</Heading> <Post title="Hello traveller!" body="Read about my adventures." /> <AllPosts /> </Section> ); } function AllPosts() { return ( <Section> <Heading>Posts</Heading> <RecentPosts /> </Section> ); } function RecentPosts() { return ( <Section> <Heading>Recent Posts</Heading> <Post title="Flavors of Lisbon" body="...those pastéis de nata!" /> <Post title="Buenos Aires in the rhythm of tango" body="I loved it!" /> </Section> ); } function Post({ title, body }) { return ( <Section isFancy={true}> <Heading> {title} </Heading> <p><i>{body}</i></p> </Section> ); }
You didn’t do anything special for this to work. A Section
specifies the context for the tree inside it, so you can insert a <Heading>
anywhere, and it will have the correct size. Try it in the sandbox above!
Context lets you write components that «adapt to their surroundings» and display themselves differently depending on where (or, in other words, in which context) they are being rendered.
How context works might remind you of CSS property inheritance. In CSS, you can specify color: blue
for a <div>
, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green
. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.
In CSS, different properties like color
and background-color
don’t override each other. You can set all <div>
’s color
to red without impacting background-color
. Similarly, different React contexts don’t override each other. Each context that you make with createContext()
is completely separate from other ones, and ties together components using and providing that particular context. One component may use or provide many different contexts without a problem.
Before you use context
Context is very tempting to use! However, this also means it’s too easy to overuse it. Just because you need to pass some props several levels deep doesn’t mean you should put that information into context.
Here’s a few alternatives you should consider before using context:
- Start by passing props. If your components are not trivial, it’s not unusual to pass a dozen props down through a dozen components. It may feel like a slog, but it makes it very clear which components use which data! The person maintaining your code will be glad you’ve made the data flow explicit with props.
- Extract components and pass JSX as
children
to them. If you pass some data through many layers of intermediate components that don’t use that data (and only pass it further down), this often means that you forgot to extract some components along the way. For example, maybe you pass data props likeposts
to visual components that don’t use them directly, like<Layout posts={posts} />
. Instead, makeLayout
takechildren
as a prop, and render<Layout><Posts posts={posts} /></Layout>
. This reduces the number of layers between the component specifying the data and the one that needs it.
If neither of these approaches works well for you, consider context.
Use cases for context
- Theming: If your app lets the user change its appearance (e.g. dark mode), you can put a context provider at the top of your app, and use that context in components that need to adjust their visual look.
- Current account: Many components might need to know the currently logged in user. Putting it in context makes it convenient to read it anywhere in the tree. Some apps also let you operate multiple accounts at the same time (e.g. to leave a comment as a different user). In those cases, it can be convenient to wrap a part of the UI into a nested provider with a different current account value.
- Routing: Most routing solutions use context internally to hold the current route. This is how every link «knows» whether it’s active or not. If you build your own router, you might want to do it too.
- Managing state: As your app grows, you might end up with a lot of state closer to the top of your app. Many distant components below may want to change it. It is common to use a reducer together with context to manage complex state and pass it down to distant components without too much hassle.
Context is not limited to static values. If you pass a different value on the next render, React will update all the components reading it below! This is why context is often used in combination with state.
In general, if some information is needed by distant components in different parts of the tree, it’s a good indication that context will help you.
Recapitulación
- Context lets a component provide some information to the entire tree below it.
- To pass context:
- Create and export it with
export const MyContext = createContext(defaultValue)
. - Pass it to the
useContext(MyContext)
Hook to read it in any child component, no matter how deep. - Wrap children into
<MyContext.Provider value={...}>
to provide it from a parent.
- Create and export it with
- Context passes through any components in the middle.
- Context lets you write components that «adapt to their surroundings».
- Before you use context, try passing props or passing JSX as
children
.
Desafío 1 de 1: Replace prop drilling with context
In this example, toggling the checkbox changes the imageSize
prop passed to each <PlaceImage>
. The checkbox state is held in the top-level App
component, but each <PlaceImage>
needs to be aware of it.
Currently, App
passes imageSize
to List
, which passes it to each Place
, which passes it to the PlaceImage
. Remove the imageSize
prop, and instead pass it from the App
component directly to PlaceImage
.
You can declare context in Context.js
.
import { useState } from 'react'; import { places } from './data.js'; import { getImageUrl } from './utils.js'; export default function App() { const [isLarge, setIsLarge] = useState(false); const imageSize = isLarge ? 150 : 100; return ( <> <label> <input type="checkbox" checked={isLarge} onChange={e => { setIsLarge(e.target.checked); }} /> Use large images </label> <hr /> <List imageSize={imageSize} /> </> ) } function List({ imageSize }) { const listItems = places.map(place => <li key={place.id}> <Place place={place} imageSize={imageSize} /> </li> ); return <ul>{listItems}</ul>; } function Place({ place, imageSize }) { return ( <> <PlaceImage place={place} imageSize={imageSize} /> <p> <b>{place.name}</b> {': ' + place.description} </p> </> ); } function PlaceImage({ place, imageSize }) { return ( <img src={getImageUrl(place)} alt={place.name} width={imageSize} height={imageSize} /> ); }