sycamore_reactive/
context.rs

1//! Context values.
2
3use std::any::{type_name, Any};
4
5use slotmap::Key;
6
7use crate::{create_child_scope, NodeId, Root};
8
9/// Provide a context value in this scope.
10///
11/// # Panics
12/// This panics if a context value of the same type exists already in this scope. Note that it is
13/// allowed to have context values with the same type in _different_ scopes.
14#[cfg_attr(debug_assertions, track_caller)]
15pub fn provide_context<T: 'static>(value: T) {
16    let root = Root::global();
17    provide_context_in_node(root.current_node.get(), value);
18}
19
20/// Provide a context value in a new scope.
21///
22/// Since this creates a new scope, this function should never panic. If the context value already
23/// exists in the outer scope, it will be shadowed by the new value in the inner scope _only_.
24/// Outside of the new scope, the old context value will still be accessible.
25///
26/// # Example
27/// ```
28/// # use sycamore_reactive::*;
29/// # let _ = create_root(|| {
30/// provide_context(123);
31/// assert_eq!(use_context::<i32>(), 123);
32///
33/// provide_context_in_new_scope(456, || {
34///     assert_eq!(use_context::<i32>(), 456);
35/// });
36///
37/// assert_eq!(use_context::<i32>(), 123);
38/// # });
39/// ```
40pub fn provide_context_in_new_scope<T: 'static, U>(value: T, f: impl FnOnce() -> U) -> U {
41    let mut ret = None;
42    create_child_scope(|| {
43        provide_context(value);
44        ret = Some(f());
45    });
46    ret.unwrap()
47}
48
49/// Internal implementation for [`provide_context`].
50#[cfg_attr(debug_assertions, track_caller)]
51fn provide_context_in_node<T: 'static>(id: NodeId, value: T) {
52    let root = Root::global();
53    let mut nodes = root.nodes.borrow_mut();
54    let any: Box<dyn Any> = Box::new(value);
55
56    let node = &mut nodes[id];
57    if node
58        .context
59        .iter()
60        .any(|x| (**x).type_id() == (*any).type_id())
61    {
62        panic!(
63            "a context with type `{}` exists already in this scope",
64            type_name::<T>()
65        );
66    }
67    node.context.push(any);
68}
69
70/// Tries to get a context value of the given type. If no context is found, returns `None`.
71#[cfg_attr(debug_assertions, track_caller)]
72pub fn try_use_context<T: Clone + 'static>() -> Option<T> {
73    let root = Root::global();
74    let nodes = root.nodes.borrow();
75    // Walk up the scope stack until we find one with the context of the right type.
76    let mut current = Some(&nodes[root.current_node.get()]);
77    while let Some(next) = current {
78        for value in &next.context {
79            if let Some(value) = value.downcast_ref::<T>().cloned() {
80                return Some(value);
81            }
82        }
83        // No context of the right type found for this scope. Now check the parent scope.
84        if next.parent.is_null() {
85            current = None;
86        } else {
87            current = Some(&nodes[next.parent]);
88        }
89    }
90    None
91}
92
93/// Get a context with the given type. If no context is found, this panics.
94#[cfg_attr(debug_assertions, track_caller)]
95pub fn use_context<T: Clone + 'static>() -> T {
96    if let Some(value) = try_use_context() {
97        value
98    } else {
99        panic!("no context of type `{}` found", type_name::<T>())
100    }
101}
102
103/// Try to get a context with the given type. If no context is found, returns the value of the
104/// function and sets the value of the context in the current scope.
105pub fn use_context_or_else<T: Clone + 'static, F: FnOnce() -> T>(f: F) -> T {
106    try_use_context().unwrap_or_else(|| {
107        let value = f();
108        provide_context(value.clone());
109        value
110    })
111}
112
113/// Gets how deep the current scope is from the root/global scope. The value for the global scope
114/// itself is always `0`.
115pub fn use_scope_depth() -> u32 {
116    let root = Root::global();
117    let nodes = root.nodes.borrow();
118    let mut current = Some(&nodes[root.current_node.get()]);
119    let mut depth = 0;
120
121    while let Some(next) = current {
122        depth += 1;
123        if next.parent.is_null() {
124            current = None;
125        } else {
126            current = Some(&nodes[next.parent]);
127        }
128    }
129    depth
130}
131
132#[cfg(test)]
133mod tests {
134    use crate::*;
135
136    /// Regression test for <https://github.com/sycamore-rs/sycamore/issues/774>
137    #[test]
138    fn cleanup_resets_context() {
139        let _ = create_root(|| {
140            let trigger = create_signal(());
141            create_memo(move || {
142                trigger.track();
143                assert!(try_use_context::<i32>().is_none());
144                provide_context(123);
145            });
146            trigger.set(());
147        });
148    }
149}