En algunos casos, queremos que los datos estén disponibles para muchos (si no todos) los componentes de una aplicación. Aunque podemos pasar datos a los componentes usando
props
, esto puede resultar difícil si casi todos los componentes de su aplicación necesitan acceder al valor de los accesorios.
A menudo terminamos con algo llamado perforación de accesorios , que es el caso cuando pasamos accesorios muy abajo en el árbol de componentes. Refactorizar el código que depende de los accesorios se vuelve casi imposible y saber de dónde provienen ciertos datos es difícil.
Digamos que tenemos un
App
componente que contiene ciertos datos. Más abajo en el árbol de componentes, tenemos un componente
ListItem
y
Header
un
Text
componente que necesita estos datos. Para llevar estos datos a estos componentes, tendríamos que pasarlos a través de múltiples capas de componentes.
En nuestro código base, se vería así:
function App() {
const data = { ... }
return (
<div>
<SideBar data={data} />
<Content data={data} />
</div>
)
}
const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>
const Content = ({ data }) => (
<div>
<Header data={data} />
<Block data={data} />
</div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>
Pasar accesorios de esta manera puede resultar bastante complicado. Si queremos cambiar el nombre del
data
accesorio en el futuro, tendríamos que cambiarle el nombre en todos los componentes. Cuanto más grande sea su aplicación, más complicada puede ser la perforación de hélice.
It would be optimal if we could skip all the layers of components that don’t need to use this data. We need to have something that gives the components that need access to the value of
data
direct access to it, without relying on prop drilling.
This is where the Provider Pattern can help us out! With the Provider Pattern, we can make data available to multiple components. Rather than passing that data down each layer through props, we can wrap all components in a
Provider
. A Provider is a higher order component provided to us by the
Context
object. We can create a Context object, using the
createContext
method that React provides for us.
The Provider receives a
value
prop, which contains the data that we want to pass down. All components that are wrapped within this provider have access to the value of the
value
prop.
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
¡Ya no tenemos que pasar manualmente el
data
accesorio a cada componente! Entonces, ¿cómo pueden los componentes y
ListItem
acceder al valor de ?
Header``Text``data
Cada componente puede obtener acceso a
data
, mediante el uso del
useContext
gancho. Este gancho recibe el contexto al que
data
tiene referencia,
DataContext
en este caso. El
useContext
gancho nos permite leer y escribir datos en el objeto de contexto.
const DataContext = React.createContext();
function App() {
const data = { ... }
return (
<div>
<SideBar />
<Content />
</div>
)
}
const SideBar = () => <List />
const List = () => <ListItem />
const Content = () => <div><Header /><Block /></div>
function ListItem() {
const { data } = React.useContext(DataContext);
return <span>{data.listItem}</span>;
}
function Text() {
const { data } = React.useContext(DataContext);
return <h1>{data.text}</h1>;
}
function Header() {
const { data } = React.useContext(DataContext);
return <div>{data.title}</div>;
}
Los componentes que no utilizan el
data
valor no tendrán que lidiar con
data
ningún problema. Ya no tenemos que preocuparnos por pasar accesorios a varios niveles a través de componentes que no necesitan el valor de los accesorios, lo que hace que la refactorización sea mucho más fácil.
El patrón Proveedor es muy útil para compartir datos globales. Un caso de uso común para el patrón de proveedor es compartir el estado de la interfaz de usuario de un tema con muchos componentes.
Queremos que el usuario pueda cambiar entre el modo claro y el modo oscuro alternando el interruptor. Cuando el usuario cambia del modo oscuro al modo claro y viceversa, ¡el color de fondo y el color del texto deberían cambiar! En lugar de pasar el valor del tema actual a cada componente, podemos envolver los componentes en un
ThemeProvider
y pasar los colores del tema actual al proveedor.
export const ThemeContext = React.createContext();
const themes = {
light: {
background: "#fff",
color: "#000",
},
dark: {
background: "#171717",
color: "#fff",
},
};
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<div className={`App theme-${theme}`}>
<ThemeContext.Provider value={providerValue}>
<Toggle />
<List />
</ThemeContext.Provider>
</div>
);
}
Dado que los componentes
Toggle
y
List
están incluidos dentro del
ThemeContext
proveedor, tenemos acceso a los valores
theme
y
toggleTheme
se pasan como
value
al proveedor.
Dentro del
Toggle
componente, podemos usar la
toggleTheme
función para actualizar el tema en consecuencia.
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function Toggle() {
const theme = useContext(ThemeContext);
return (
<label className="switch">
<input type="checkbox" onClick={theme.toggleTheme} />
<span className="slider round" />
</label>
);
}
Al
List
componente en sí no le importa el valor actual del tema. Sin embargo, ¡los
ListItem
componentes sí! Podemos usar el
theme
contexto directamente dentro del
ListItem
.
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}
¡Perfecto! No tuvimos que transmitir ningún dato a componentes a los que no les importaba el valor actual del tema.
Hands
Podemos crear un gancho para proporcionar contexto a los componentes. En lugar de tener que importar
useContext
el contexto en cada componente, podemos usar un enlace que devuelva el contexto que necesitamos.
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}
Para asegurarnos de que sea un tema válido, generemos un error si
useContext(ThemeContext)
devuelve un valor falso.
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}
En lugar de envolver los componentes directamente con el
ThemeContext.Provider
componente, podemos crear un HOC que envuelva el componente para proporcionar sus valores. De esta manera, podemos separar la lógica del contexto de los componentes de renderizado, lo que mejora la reutilización del proveedor.
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<ThemeContext.Provider value={providerValue}>
{children}
</ThemeContext.Provider>
);
}
export default function App() {
return (
<div className={`App theme-${theme}`}>
<ThemeProvider>
<Toggle />
<List />
</ThemeProvider>
</div>
);
}
Cada componente que necesita tener acceso a
ThemeContext
, ahora puede simplemente usar el
useThemeContext
gancho.
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}
Al crear enlaces para los diferentes contextos, es fácil separar la lógica de los proveedores de los componentes que representan los datos.
Caso de estudio
Some libraries provide built-in providers, which values we can use in the consuming components. A good example of this, is styled-components.
No experience with styled-components is needed to understand this example.
The styled-components library provides a
ThemeProvider
for us. Each styled component will have access to the value of this provider! Instead of creating a context API ourselves, we can use the one that’s been provided to us!
Let’s use the same List example, and wrap the components in the
ThemeProvider
imported from the
styled-component
library.
import { ThemeProvider } from "styled-components";
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return (
<div className={`App theme-${theme}`}>
<ThemeProvider theme={themes[theme]}>
<Toggle toggleTheme={toggleTheme} />
<List />
</ThemeProvider>
</div>
);
}
En lugar de pasar un
style
accesorio en línea al
ListItem
componente, lo convertiremos en un
styled.li
componente. Como es un componente con estilo, podemos acceder al valor de
theme
!
import styled from "styled-components";
export default function ListItem() {
return (
<Li>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</Li>
);
}
const Li = styled.li`
${({ theme }) => `
background-color: ${theme.backgroundColor};
color: ${theme.color};
`}
`;
¡Genial, ahora podemos aplicar estilos fácilmente a todos nuestros componentes con estilo con
ThemeProvider
!
Compensaciones
Ventajas
El patrón de proveedor/API de contexto hace posible pasar datos a muchos componentes, sin tener que pasarlos manualmente a través de cada capa de componente.
Reduce el riesgo de introducir errores accidentalmente al refactorizar el código. Anteriormente, si más adelante queríamos cambiar el nombre de un accesorio, teníamos que cambiar el nombre de este accesorio en toda la aplicación donde se usaba este valor.
Ya no tenemos que lidiar con la perforación de puntales , que podría verse como un antipatrón. Anteriormente, podía resultar difícil comprender el flujo de datos de la aplicación, ya que no siempre estaba claro dónde se originaban ciertos valores de propiedad. Con el patrón Proveedor, ya no tenemos que pasar accesorios innecesariamente a componentes que no se preocupan por estos datos.
Mantener algún tipo de estado global es fácil con el patrón Proveedor, ya que podemos dar acceso a los componentes a este estado global.
Contras
En algunos casos, el uso excesivo del patrón Proveedor puede provocar problemas de rendimiento. Todos los componentes que consumen el contexto se vuelven a representar en cada cambio de estado.
Veamos un ejemplo. Tenemos un contador simple cuyo valor aumenta cada vez que hacemos clic en el
Increment
botón del
Button
componente. También tenemos un
Reset
botón en el
Reset
componente, que restablece el conteo a
0
.
Sin embargo, cuando hace clic en
Increment
, puede ver que no es sólo el recuento lo que se vuelve a representar. ¡ La fecha en el
Reset
componente también se vuelve a representar!
El
Reset
componente también se volvió a renderizar ya que consumió el archivo
useCountContext
. En aplicaciones más pequeñas, esto no importará demasiado. En aplicaciones más grandes, pasar un valor actualizado con frecuencia a muchos componentes puede afectar negativamente el rendimiento.
Para asegurarse de que los componentes no consuman proveedores que contengan valores innecesarios que puedan actualizarse, puede crear varios proveedores para cada caso de uso por separado.