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.