dioxus_core/
scope_context.rs

1use crate::runtime::RuntimeError;
2use crate::{innerlude::SchedulerMsg, Runtime, ScopeId, Task};
3use crate::{
4    innerlude::{throw_into, CapturedError},
5    prelude::SuspenseContext,
6};
7use generational_box::{AnyStorage, Owner};
8use rustc_hash::FxHashSet;
9use std::{
10    any::Any,
11    cell::{Cell, RefCell},
12    future::Future,
13    sync::Arc,
14};
15
16pub(crate) enum ScopeStatus {
17    Mounted,
18    Unmounted {
19        // Before the component is mounted, we need to keep track of effects that need to be run once the scope is mounted
20        effects_queued: Vec<Box<dyn FnOnce() + 'static>>,
21    },
22}
23
24#[derive(Debug, Clone, Default)]
25pub(crate) enum SuspenseLocation {
26    #[default]
27    NotSuspended,
28    SuspenseBoundary(SuspenseContext),
29    UnderSuspense(SuspenseContext),
30    InSuspensePlaceholder(SuspenseContext),
31}
32
33impl SuspenseLocation {
34    pub(crate) fn suspense_context(&self) -> Option<&SuspenseContext> {
35        match self {
36            SuspenseLocation::InSuspensePlaceholder(context) => Some(context),
37            SuspenseLocation::UnderSuspense(context) => Some(context),
38            SuspenseLocation::SuspenseBoundary(context) => Some(context),
39            _ => None,
40        }
41    }
42}
43
44/// A component's state separate from its props.
45///
46/// This struct exists to provide a common interface for all scopes without relying on generics.
47pub(crate) struct Scope {
48    pub(crate) name: &'static str,
49    pub(crate) id: ScopeId,
50    pub(crate) parent_id: Option<ScopeId>,
51    pub(crate) height: u32,
52    pub(crate) render_count: Cell<usize>,
53
54    // Note: the order of the hook and context fields is important. The hooks field must be dropped before the contexts field in case a hook drop implementation tries to access a context.
55    pub(crate) hooks: RefCell<Vec<Box<dyn Any>>>,
56    pub(crate) hook_index: Cell<usize>,
57    pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
58    pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
59    pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
60    pub(crate) after_render: RefCell<Vec<Box<dyn FnMut()>>>,
61
62    /// The suspense boundary that this scope is currently in (if any)
63    suspense_boundary: SuspenseLocation,
64
65    pub(crate) status: RefCell<ScopeStatus>,
66}
67
68impl Scope {
69    pub(crate) fn new(
70        name: &'static str,
71        id: ScopeId,
72        parent_id: Option<ScopeId>,
73        height: u32,
74        suspense_boundary: SuspenseLocation,
75    ) -> Self {
76        Self {
77            name,
78            id,
79            parent_id,
80            height,
81            render_count: Cell::new(0),
82            shared_contexts: RefCell::new(vec![]),
83            spawned_tasks: RefCell::new(FxHashSet::default()),
84            hooks: RefCell::new(vec![]),
85            hook_index: Cell::new(0),
86            before_render: RefCell::new(vec![]),
87            after_render: RefCell::new(vec![]),
88            status: RefCell::new(ScopeStatus::Unmounted {
89                effects_queued: Vec::new(),
90            }),
91            suspense_boundary,
92        }
93    }
94
95    pub fn parent_id(&self) -> Option<ScopeId> {
96        self.parent_id
97    }
98
99    fn sender(&self) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
100        Runtime::with(|rt| rt.sender.clone()).unwrap_or_else(|e| panic!("{}", e))
101    }
102
103    /// Mount the scope and queue any pending effects if it is not already mounted
104    pub(crate) fn mount(&self, runtime: &Runtime) {
105        let mut status = self.status.borrow_mut();
106        if let ScopeStatus::Unmounted { effects_queued } = &mut *status {
107            for f in effects_queued.drain(..) {
108                runtime.queue_effect_on_mounted_scope(self.id, f);
109            }
110            *status = ScopeStatus::Mounted;
111        }
112    }
113
114    /// Get the suspense location of this scope
115    pub(crate) fn suspense_location(&self) -> SuspenseLocation {
116        self.suspense_boundary.clone()
117    }
118
119    /// If this scope is a suspense boundary, return the suspense context
120    pub(crate) fn suspense_boundary(&self) -> Option<SuspenseContext> {
121        match self.suspense_location() {
122            SuspenseLocation::SuspenseBoundary(context) => Some(context),
123            _ => None,
124        }
125    }
126
127    /// Check if a node should run during suspense
128    pub(crate) fn should_run_during_suspense(&self) -> bool {
129        let Some(context) = self.suspense_boundary.suspense_context() else {
130            return false;
131        };
132
133        !context.frozen()
134    }
135
136    /// Mark this scope as dirty, and schedule a render for it.
137    pub fn needs_update(&self) {
138        self.needs_update_any(self.id)
139    }
140
141    /// Mark this scope as dirty, and schedule a render for it.
142    pub fn needs_update_any(&self, id: ScopeId) {
143        self.sender()
144            .unbounded_send(SchedulerMsg::Immediate(id))
145            .expect("Scheduler to exist if scope exists");
146    }
147
148    /// Create a subscription that schedules a future render for the referenced component.
149    ///
150    /// Note: you should prefer using [`Self::schedule_update_any`] and [`Self::id`].
151    ///
152    /// Note: The function returned by this method will schedule an update for the current component even if it has already updated between when `schedule_update` was called and when the returned function is called.
153    /// If the desired behavior is to invalidate the current rendering of the current component (and no-op if already invalidated)
154    /// [`subscribe`](crate::reactive_context::ReactiveContext::subscribe) to the [`current`](crate::reactive_context::ReactiveContext::current) [`ReactiveContext`](crate::reactive_context::ReactiveContext) instead.
155    pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
156        let (chan, id) = (self.sender(), self.id);
157        Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
158    }
159
160    /// Schedule an update for any component given its [`ScopeId`].
161    ///
162    /// A component's [`ScopeId`] can be obtained from `use_hook` or the [`current_scope_id`](crate::prelude::current_scope_id) method.
163    ///
164    /// This method should be used when you want to schedule an update for a component.
165    ///
166    /// Note: It does not matter when `schedule_update_any` is called: the returned function will invalidate what ever generation of the specified component is current when returned function is called.
167    /// If the desired behavior is to schedule invalidation of the current rendering of a component, use [`ReactiveContext`](crate::reactive_context::ReactiveContext) instead.
168    pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
169        let chan = self.sender();
170        Arc::new(move |id| {
171            chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
172        })
173    }
174
175    /// Get the owner for the current scope.
176    pub fn owner<S: AnyStorage>(&self) -> Owner<S> {
177        match self.has_context() {
178            Some(rt) => rt,
179            None => {
180                let owner = S::owner();
181                self.provide_context(owner)
182            }
183        }
184    }
185
186    /// Return any context of type T if it exists on this scope
187    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
188        self.shared_contexts
189            .borrow()
190            .iter()
191            .find_map(|any| any.downcast_ref::<T>())
192            .cloned()
193    }
194
195    /// Try to retrieve a shared state with type `T` from any parent scope.
196    ///
197    /// Clones the state if it exists.
198    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
199        tracing::trace!(
200            "looking for context {} ({:?}) in {:?}",
201            std::any::type_name::<T>(),
202            std::any::TypeId::of::<T>(),
203            self.id
204        );
205        if let Some(this_ctx) = self.has_context() {
206            return Some(this_ctx);
207        }
208
209        let mut search_parent = self.parent_id;
210        let cur_runtime = Runtime::with(|runtime| {
211            while let Some(parent_id) = search_parent {
212                let Some(parent) = runtime.get_state(parent_id) else {
213                    tracing::error!("Parent scope {:?} not found", parent_id);
214                    return None;
215                };
216                tracing::trace!(
217                    "looking for context {} ({:?}) in {:?}",
218                    std::any::type_name::<T>(),
219                    std::any::TypeId::of::<T>(),
220                    parent.id
221                );
222                if let Some(shared) = parent.has_context() {
223                    return Some(shared);
224                }
225                search_parent = parent.parent_id;
226            }
227            None
228        });
229
230        match cur_runtime.ok().flatten() {
231            Some(ctx) => Some(ctx),
232            None => {
233                tracing::trace!(
234                    "context {} ({:?}) not found",
235                    std::any::type_name::<T>(),
236                    std::any::TypeId::of::<T>()
237                );
238                None
239            }
240        }
241    }
242
243    /// Inject a `Box<dyn Any>` into the context of this scope
244    pub(crate) fn provide_any_context(&self, mut value: Box<dyn Any>) {
245        let mut contexts = self.shared_contexts.borrow_mut();
246
247        // If the context exists, swap it out for the new value
248        for ctx in contexts.iter_mut() {
249            // Swap the ptr directly
250            if ctx.as_ref().type_id() == value.as_ref().type_id() {
251                std::mem::swap(ctx, &mut value);
252                return;
253            }
254        }
255
256        // Else, just push it
257        contexts.push(value);
258    }
259
260    /// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
261    ///
262    /// This is a "fundamental" operation and should only be called during initialization of a hook.
263    ///
264    /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
265    ///
266    /// # Example
267    ///
268    /// ```rust
269    /// # use dioxus::prelude::*;
270    /// #[derive(Clone)]
271    /// struct SharedState(&'static str);
272    ///
273    /// // The parent provides context that is available in all children
274    /// fn app() -> Element {
275    ///     use_hook(|| provide_context(SharedState("world")));
276    ///     rsx!(Child {})
277    /// }
278    ///
279    /// // Any child elements can access the context with the `consume_context` function
280    /// fn Child() -> Element {
281    ///     let state = use_context::<SharedState>();
282    ///     rsx!(div { "hello {state.0}" })
283    /// }
284    /// ```
285    pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
286        tracing::trace!(
287            "providing context {} ({:?}) in {:?}",
288            std::any::type_name::<T>(),
289            std::any::TypeId::of::<T>(),
290            self.id
291        );
292        let mut contexts = self.shared_contexts.borrow_mut();
293
294        // If the context exists, swap it out for the new value
295        for ctx in contexts.iter_mut() {
296            // Swap the ptr directly
297            if let Some(ctx) = ctx.downcast_mut::<T>() {
298                std::mem::swap(ctx, &mut value.clone());
299                return value;
300            }
301        }
302
303        // Else, just push it
304        contexts.push(Box::new(value.clone()));
305
306        value
307    }
308
309    /// Provide a context to the root and then consume it
310    ///
311    /// This is intended for "global" state management solutions that would rather be implicit for the entire app.
312    /// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
313    ///
314    /// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
315    /// when a context already exists will swap the context out for the new one, which may not be what you want.
316    pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
317        Runtime::with(|runtime| {
318            runtime
319                .get_state(ScopeId::ROOT)
320                .unwrap()
321                .provide_context(context)
322        })
323        .expect("Runtime to exist")
324    }
325
326    /// Start a new future on the same thread as the rest of the VirtualDom.
327    ///
328    /// **You should generally use `spawn` instead of this method unless you specifically need to need to run a task during suspense**
329    ///
330    /// This future will not contribute to suspense resolving but it will run during suspense.
331    ///
332    /// Because this future runs during suspense, you need to be careful to work with hydration. It is not recommended to do any async IO work in this future, as it can easily cause hydration issues. However, you can use isomorphic tasks to do work that can be consistently replicated on the server and client like logging or responding to state changes.
333    ///
334    /// ```rust, no_run
335    /// # use dioxus::prelude::*;
336    /// // ❌ Do not do requests in isomorphic tasks. It may resolve at a different time on the server and client, causing hydration issues.
337    /// let mut state = use_signal(|| None);
338    /// spawn_isomorphic(async move {
339    ///     state.set(Some(reqwest::get("https://api.example.com").await));
340    /// });
341    ///
342    /// // ✅ You may wait for a signal to change and then log it
343    /// let mut state = use_signal(|| 0);
344    /// spawn_isomorphic(async move {
345    ///     loop {
346    ///         tokio::time::sleep(std::time::Duration::from_secs(1)).await;
347    ///         println!("State is {state}");
348    ///     }
349    /// });
350    /// ```
351    pub fn spawn_isomorphic(&self, fut: impl Future<Output = ()> + 'static) -> Task {
352        let id = Runtime::with(|rt| rt.spawn_isomorphic(self.id, fut)).expect("Runtime to exist");
353        self.spawned_tasks.borrow_mut().insert(id);
354        id
355    }
356
357    /// Spawns the future and returns the [`Task`]
358    pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) -> Task {
359        let id = Runtime::with(|rt| rt.spawn(self.id, fut)).expect("Runtime to exist");
360        self.spawned_tasks.borrow_mut().insert(id);
361        id
362    }
363
364    /// Queue an effect to run after the next render
365    pub fn queue_effect(&self, f: impl FnOnce() + 'static) {
366        Runtime::with(|rt| rt.queue_effect(self.id, f)).expect("Runtime to exist");
367    }
368
369    /// Store a value between renders. The foundational hook for all other hooks.
370    ///
371    /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render).
372    /// `use_hook` will return a clone of the value on every render.
373    ///
374    /// In order to clean up resources you would need to implement the [`Drop`] trait for an inner value stored in a RC or similar (Signals for instance),
375    /// as these only drop their inner value once all references have been dropped, which only happens when the component is dropped.
376    ///
377    /// <div class="warning">
378    ///
379    /// `use_hook` is not reactive. It just returns the value on every render. If you need state that will track changes, use [`use_signal`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_signal.html) instead.
380    ///
381    /// ❌ Don't use `use_hook` with `Rc<RefCell<T>>` for state. It will not update the UI and other hooks when the state changes.
382    /// ```rust
383    /// use dioxus::prelude::*;
384    /// use std::rc::Rc;
385    /// use std::cell::RefCell;
386    ///
387    /// pub fn Comp() -> Element {
388    ///     let count = use_hook(|| Rc::new(RefCell::new(0)));
389    ///
390    ///     rsx! {
391    ///         button {
392    ///             onclick: move |_| *count.borrow_mut() += 1,
393    ///             "{count.borrow()}"
394    ///         }
395    ///     }
396    /// }
397    /// ```
398    ///
399    /// ✅ Use `use_signal` instead.
400    /// ```rust
401    /// use dioxus::prelude::*;
402    ///
403    /// pub fn Comp() -> Element {
404    ///     let mut count = use_signal(|| 0);
405    ///
406    ///     rsx! {
407    ///         button {
408    ///             onclick: move |_| count += 1,
409    ///             "{count}"
410    ///         }
411    ///     }
412    /// }
413    /// ```
414    ///
415    /// </div>
416    ///
417    /// # Example
418    ///
419    /// ```rust
420    /// use dioxus::prelude::*;
421    ///
422    /// // prints a greeting on the initial render
423    /// pub fn use_hello_world() {
424    ///     use_hook(|| println!("Hello, world!"));
425    /// }
426    /// ```
427    ///
428    /// # Custom Hook Example
429    ///
430    /// ```rust
431    /// use dioxus::prelude::*;
432    ///
433    /// pub struct InnerCustomState(usize);
434    ///
435    /// impl Drop for InnerCustomState {
436    ///     fn drop(&mut self){
437    ///         println!("Component has been dropped.");
438    ///     }
439    /// }
440    ///
441    /// #[derive(Clone, Copy)]
442    /// pub struct CustomState {
443    ///     inner: Signal<InnerCustomState>
444    /// }
445    ///
446    /// pub fn use_custom_state() -> CustomState {
447    ///     use_hook(|| CustomState {
448    ///         inner: Signal::new(InnerCustomState(0))
449    ///     })
450    /// }
451    /// ```
452    pub fn use_hook<State: Clone + 'static>(&self, initializer: impl FnOnce() -> State) -> State {
453        let cur_hook = self.hook_index.get();
454        let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
455
456        if cur_hook >= hooks.len() {
457            Runtime::with(|rt| {
458                rt.while_not_rendering(|| {
459                    hooks.push(Box::new(initializer()));
460                });
461            })
462            .unwrap()
463        }
464
465        self.use_hook_inner::<State>(hooks, cur_hook)
466    }
467
468    // The interior version that gets monoorphized by the `State` type but not the `initializer` type.
469    // This helps trim down binary sizes
470    fn use_hook_inner<State: Clone + 'static>(
471        &self,
472        hooks: std::cell::RefMut<Vec<Box<dyn std::any::Any>>>,
473        cur_hook: usize,
474    ) -> State {
475        hooks
476            .get(cur_hook)
477            .and_then(|inn| {
478                self.hook_index.set(cur_hook + 1);
479                let raw_ref: &dyn Any = inn.as_ref();
480                raw_ref.downcast_ref::<State>().cloned()
481            })
482            .expect(
483                r#"
484                Unable to retrieve the hook that was initialized at this index.
485                Consult the `rules of hooks` to understand how to use hooks properly.
486
487                You likely used the hook in a conditional. Hooks rely on consistent ordering between renders.
488                Functions prefixed with "use" should never be called conditionally.
489
490                Help: Run `dx check` to look for check for some common hook errors.
491                "#,
492            )
493    }
494
495    pub fn push_before_render(&self, f: impl FnMut() + 'static) {
496        self.before_render.borrow_mut().push(Box::new(f));
497    }
498
499    pub fn push_after_render(&self, f: impl FnMut() + 'static) {
500        self.after_render.borrow_mut().push(Box::new(f));
501    }
502
503    /// Get the current render since the inception of this component
504    ///
505    /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
506    pub fn generation(&self) -> usize {
507        self.render_count.get()
508    }
509
510    /// Get the height of this scope
511    pub fn height(&self) -> u32 {
512        self.height
513    }
514}
515
516impl ScopeId {
517    /// Get the current scope id
518    pub fn current_scope_id(self) -> Result<ScopeId, RuntimeError> {
519        Runtime::with(|rt| rt.current_scope_id().ok())
520            .ok()
521            .flatten()
522            .ok_or(RuntimeError::new())
523    }
524
525    /// Consume context from the current scope
526    pub fn consume_context<T: 'static + Clone>(self) -> Option<T> {
527        Runtime::with_scope(self, |cx| cx.consume_context::<T>())
528            .ok()
529            .flatten()
530    }
531
532    /// Consume context from the current scope
533    pub fn consume_context_from_scope<T: 'static + Clone>(self, scope_id: ScopeId) -> Option<T> {
534        Runtime::with(|rt| {
535            rt.get_state(scope_id)
536                .and_then(|cx| cx.consume_context::<T>())
537        })
538        .ok()
539        .flatten()
540    }
541
542    /// Check if the current scope has a context
543    pub fn has_context<T: 'static + Clone>(self) -> Option<T> {
544        Runtime::with_scope(self, |cx| cx.has_context::<T>())
545            .ok()
546            .flatten()
547    }
548
549    /// Provide context to the current scope
550    pub fn provide_context<T: 'static + Clone>(self, value: T) -> T {
551        Runtime::with_scope(self, |cx| cx.provide_context(value)).unwrap()
552    }
553
554    /// Pushes the future onto the poll queue to be polled after the component renders.
555    pub fn push_future(self, fut: impl Future<Output = ()> + 'static) -> Option<Task> {
556        Runtime::with_scope(self, |cx| cx.spawn(fut)).ok()
557    }
558
559    /// Spawns the future but does not return the [`Task`]
560    pub fn spawn(self, fut: impl Future<Output = ()> + 'static) {
561        Runtime::with_scope(self, |cx| cx.spawn(fut)).unwrap();
562    }
563
564    /// Get the current render since the inception of this component
565    ///
566    /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
567    pub fn generation(self) -> Option<usize> {
568        Runtime::with_scope(self, |cx| Some(cx.generation())).unwrap()
569    }
570
571    /// Get the parent of the current scope if it exists
572    pub fn parent_scope(self) -> Option<ScopeId> {
573        Runtime::with_scope(self, |cx| cx.parent_id())
574            .ok()
575            .flatten()
576    }
577
578    /// Check if the current scope is a descendant of the given scope
579    pub fn is_descendant_of(self, other: ScopeId) -> bool {
580        let mut current = self;
581        while let Some(parent) = current.parent_scope() {
582            if parent == other {
583                return true;
584            }
585            current = parent;
586        }
587        false
588    }
589
590    /// Mark the current scope as dirty, causing it to re-render
591    pub fn needs_update(self) {
592        Runtime::with_scope(self, |cx| cx.needs_update()).unwrap();
593    }
594
595    /// Create a subscription that schedules a future render for the reference component. Unlike [`Self::needs_update`], this function will work outside of the dioxus runtime.
596    ///
597    /// ## Notice: you should prefer using [`crate::schedule_update_any`]
598    pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
599        Runtime::with_scope(*self, |cx| cx.schedule_update()).unwrap()
600    }
601
602    /// Get the height of the current scope
603    pub fn height(self) -> u32 {
604        Runtime::with_scope(self, |cx| cx.height()).unwrap()
605    }
606
607    /// Run a closure inside of scope's runtime
608    #[track_caller]
609    pub fn in_runtime<T>(self, f: impl FnOnce() -> T) -> T {
610        Runtime::current()
611            .unwrap_or_else(|e| panic!("{}", e))
612            .on_scope(self, f)
613    }
614
615    /// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`](crate::prelude::ErrorBoundary) or the root of the app.
616    ///
617    /// # Examples
618    /// ```rust, no_run
619    /// # use dioxus::prelude::*;
620    /// fn Component() -> Element {
621    ///     let request = spawn(async move {
622    ///         match reqwest::get("https://api.example.com").await {
623    ///             Ok(_) => unimplemented!(),
624    ///             // You can explicitly throw an error into a scope with throw_error
625    ///             Err(err) => ScopeId::APP.throw_error(err)
626    ///         }
627    ///     });
628    ///
629    ///     unimplemented!()
630    /// }
631    /// ```
632    pub fn throw_error(self, error: impl Into<CapturedError> + 'static) {
633        throw_into(error, self)
634    }
635
636    /// Get the suspense context the current scope is in
637    pub fn suspense_context(&self) -> Option<SuspenseContext> {
638        Runtime::with_scope(*self, |cx| cx.suspense_boundary.suspense_context().cloned()).unwrap()
639    }
640}