wasm_react/
context.rs

1use crate::{
2  create_element, hooks::RefContainerValue, props::Props, react_bindings,
3  Component, VNode,
4};
5use js_sys::Reflect;
6use std::{marker::PhantomData, rc::Rc, thread::LocalKey};
7use wasm_bindgen::{intern, JsValue, UnwrapThrowExt};
8
9/// Represents a [React context][context] that can hold a global state.
10///
11/// See [`create_context()`] for usage.
12///
13/// [context]: https://react.dev/learn/passing-data-deeply-with-context
14#[derive(Debug)]
15pub struct Context<T> {
16  js_context: JsValue,
17  phantom: PhantomData<T>,
18}
19
20impl<T> AsRef<JsValue> for Context<T> {
21  fn as_ref(&self) -> &JsValue {
22    &self.js_context
23  }
24}
25
26impl<T> From<Context<T>> for JsValue {
27  fn from(value: Context<T>) -> Self {
28    value.js_context
29  }
30}
31
32/// Creates a new [React context][context] that can hold a global state.
33///
34/// Use [`ContextProvider`] to make the context available for its subtrees and
35/// [`use_context()`](crate::hooks::use_context()) to get access to the context
36/// value.
37///
38/// [context]: https://react.dev/learn/passing-data-deeply-with-context
39///
40/// # Example
41///
42/// ```
43/// # use wasm_react::{*, hooks::*, props::*};
44/// # pub enum Theme { DarkMode, LightMode }
45/// #
46/// thread_local! {
47///   // Pass in a default value for the context.
48///   static THEME_CONTEXT: Context<Theme> = create_context(Theme::LightMode.into());
49/// }
50///
51/// struct App;
52///
53/// impl Component for App {
54///   fn render(&self) -> VNode {
55///     // Use a `ContextProvider` to pass the context value to the trees below.
56///     // In this example, we are passing down `Theme::DarkMode`.
57///
58///     ContextProvider::from(&THEME_CONTEXT)
59///       .value(Some(Theme::DarkMode.into()))
60///       .build(
61///         Toolbar.build(),
62///       )
63///   }
64/// }
65///
66/// struct Toolbar;
67///
68/// impl Component for Toolbar {
69///   fn render(&self) -> VNode {
70///     // Theme context does not have to be passed down explicitly.
71///     h!(div).build(Button.build())
72///   }
73/// }
74///
75/// struct Button;
76///
77/// impl Component for Button {
78///   fn render(&self) -> VNode {
79///     // Use the `use_context` hook to get access to the context value.
80///     let theme = use_context(&THEME_CONTEXT);
81///
82///     h!(button)
83///       .style(
84///         &Style::new()
85///           .background_color(match *theme {
86///             Theme::LightMode => "white",
87///             Theme::DarkMode => "black",
88///           })
89///       )
90///       .build("Button")
91///   }
92/// }
93/// ```
94pub fn create_context<T: 'static>(init: Rc<T>) -> Context<T> {
95  Context {
96    js_context: react_bindings::create_context(RefContainerValue(init)),
97    phantom: PhantomData,
98  }
99}
100
101/// A component that can make the given context available for its subtrees.
102///
103/// See [`create_context()`] for usage.
104#[derive(Debug, Clone)]
105pub struct ContextProvider<T: 'static> {
106  context: &'static LocalKey<Context<T>>,
107  value: Option<Rc<T>>,
108  children: VNode,
109}
110
111impl<T: 'static> ContextProvider<T> {
112  /// Creates a new [`ContextProvider`] from the given context.
113  pub fn from(context: &'static LocalKey<Context<T>>) -> Self {
114    Self {
115      context,
116      value: None,
117      children: ().into(),
118    }
119  }
120
121  /// Sets the value of the context to be passed down.
122  pub fn value(mut self, value: Option<Rc<T>>) -> Self {
123    self.value = value;
124    self
125  }
126
127  /// Returns a [`VNode`] to be included in a render function.
128  pub fn build(mut self, children: impl Into<VNode>) -> VNode {
129    self.children = children.into();
130    Component::build(self)
131  }
132}
133
134impl<T: 'static> Component for ContextProvider<T> {
135  fn render(&self) -> VNode {
136    self.context.with(|context| {
137      create_element(
138        &Reflect::get(context.as_ref(), &intern("Provider").into())
139          .expect_throw("cannot read from context object"),
140        &{
141          let mut props = Props::new();
142
143          if let Some(value) = self.value.as_ref() {
144            props = props.insert(
145              intern("value"),
146              &RefContainerValue(value.clone()).into(),
147            );
148          }
149
150          props
151        },
152        self.children.clone(),
153      )
154    })
155  }
156}