Contexts
Usually, data is passed from a parent component to a child component via props. But passing props can become verbose and annoying if you have to pass them through many components in the middle, or if many components in your app need the same information. Context solves this problem by allowing a parent component to make data available to any component in the tree below it, no matter how deep, without having to pass it down with props.
The problem with props: "Prop Drilling"
Passing props is a great way to pass data directly from a parent to a child. They become cumbersome to pass down through deeply nested component trees or when multiple components share the same data. A common solution to data sharing is lifting the data to a common ancestor and making the children take it as props. However, this can lead to cases where the prop has to go through multiple components to reach the component that needs it. This situation is called "Prop Drilling".
Consider the following example which passes down the theme using props:
use yew::{html, Component, Context, Html, Properties, function_component};
#[derive(Clone, PartialEq)]
pub struct Theme {
foreground: String,
background: String,
}
#[derive(PartialEq, Properties)]
pub struct NavbarProps {
theme: Theme,
}
#[function_component]
fn Navbar(props: &NavbarProps) -> Html {
html! {
<div>
<Title theme={props.theme.clone()}>
{ "App title" }
</Title>
<NavButton theme={props.theme.clone()}>
{ "Somewhere" }
</NavButton>
</div>
}
}
#[derive(PartialEq, Properties)]
pub struct ThemeProps {
theme: Theme,
children: Html,
}
#[function_component]
fn Title(_props: &ThemeProps) -> Html {
html! {
// impl
}
}
#[function_component]
fn NavButton(_props: &ThemeProps) -> Html {
html! {
// impl
}
}
/// App root
#[function_component]
fn App() -> Html {
let theme = Theme {
foreground: "yellow".to_owned(),
background: "pink".to_owned(),
};
html! {
<Navbar {theme} />
}
}
We "drill" the theme prop through Navbar
so that it can reach Title
and NavButton
.
It would be nice if Title
and NavButton
, the components that need access to the theme, can just access the theme
without having to pass it to them as a prop. Contexts solve this problem by allowing a parent to pass data, theme in this case,
to its children.
Using Contexts
Step 1: Providing the context
A context provider is required to consume the context. ContextProvider<T>
, where T
is the context struct used as the provider.
T
must implement Clone
and PartialEq
. ContextProvider
is the component whose children will have the context available to them.
The children are re-rendered when the context changes. A struct is used to define what data is to be passed. The ContextProvider
can be used as:
use yew::prelude::*;
/// App theme
#[derive(Clone, Debug, PartialEq)]
struct Theme {
foreground: String,
background: String,
}
/// Main component
#[function_component]
pub fn App() -> Html {
let ctx = use_state(|| Theme {
foreground: "#000000".to_owned(),
background: "#eeeeee".to_owned(),
});
html! {
// `ctx` is type `Rc<UseStateHandle<Theme>>` while we need `Theme`
// so we deref it.
// It derefs to `&Theme`, hence the clone
<ContextProvider<Theme> context={(*ctx).clone()}>
// Every child here and their children will have access to this context.
<Toolbar />
</ContextProvider<Theme>>
}
}
/// The toolbar.
/// This component has access to the context
#[function_component]
pub fn Toolbar() -> Html {
html! {
<div>
<ThemedButton />
</div>
}
}
/// Button placed in `Toolbar`.
/// As this component is a child of `ThemeContextProvider` in the component tree, it also has access
/// to the context.
#[function_component]
pub fn ThemedButton() -> Html {
let theme = use_context::<Theme>().expect("no ctx found");
html! {
<button style={format!("background: {}; color: {};", theme.background, theme.foreground)}>
{ "Click me!" }
</button>
}
}
Step 2: Consuming context
Function components
use_context
hook is used to consume contexts in function components.
See docs for use_context to learn more.
Struct components
We have 2 options to consume contexts in struct components:
- Higher Order Components: A higher-order function component will consume the context and pass the data to the struct component which requires it.
- Consume context directly in the struct component. See example of struct component as a consumer