dioxus_core/
runtime.rs

1use crate::nodes::VNodeMount;
2use crate::scheduler::ScopeOrder;
3use crate::scope_context::SuspenseLocation;
4use crate::{arena::ElementRef, CapturedError};
5use crate::{
6    innerlude::{DirtyTasks, Effect},
7    SuspenseContext,
8};
9use crate::{
10    innerlude::{LocalTask, SchedulerMsg},
11    scope_context::Scope,
12    scopes::ScopeId,
13    Task,
14};
15use crate::{AttributeValue, ElementId, Event};
16use generational_box::{AnyStorage, Owner};
17use slab::Slab;
18use slotmap::DefaultKey;
19use std::any::Any;
20use std::collections::BTreeSet;
21use std::{
22    cell::{Cell, Ref, RefCell},
23    rc::Rc,
24};
25use tracing::instrument;
26
27thread_local! {
28    static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = const { RefCell::new(vec![]) };
29}
30
31/// A global runtime that is shared across all scopes that provides the async runtime and context API
32pub struct Runtime {
33    // We use this to track the current scope
34    // This stack should only be modified through [`Runtime::with_scope_on_stack`] to ensure that the stack is correctly restored
35    scope_stack: RefCell<Vec<ScopeId>>,
36
37    // We use this to track the current suspense location. Generally this lines up with the scope stack, but it may be different for children of a suspense boundary
38    // This stack should only be modified through [`Runtime::with_suspense_location`] to ensure that the stack is correctly restored
39    suspense_stack: RefCell<Vec<SuspenseLocation>>,
40
41    // A hand-rolled slab of scope states
42    pub(crate) scope_states: RefCell<Vec<Option<Scope>>>,
43
44    // We use this to track the current task
45    pub(crate) current_task: Cell<Option<Task>>,
46
47    /// Tasks created with cx.spawn
48    pub(crate) tasks: RefCell<slotmap::SlotMap<DefaultKey, Rc<LocalTask>>>,
49
50    // Currently suspended tasks
51    pub(crate) suspended_tasks: Cell<usize>,
52
53    pub(crate) rendering: Cell<bool>,
54
55    pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
56
57    // The effects that need to be run after the next render
58    pub(crate) pending_effects: RefCell<BTreeSet<Effect>>,
59
60    // Tasks that are waiting to be polled
61    pub(crate) dirty_tasks: RefCell<BTreeSet<DirtyTasks>>,
62
63    // The element ids that are used in the renderer
64    // These mark a specific place in a whole rsx block
65    pub(crate) elements: RefCell<Slab<Option<ElementRef>>>,
66
67    // Once nodes are mounted, the information about where they are mounted is stored here
68    // We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events
69    // Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block
70    pub(crate) mounts: RefCell<Slab<VNodeMount>>,
71}
72
73impl Runtime {
74    pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
75        let mut elements = Slab::default();
76        // the root element is always given element ID 0 since it's the container for the entire tree
77        elements.insert(None);
78
79        Rc::new(Self {
80            sender,
81            rendering: Cell::new(false),
82            scope_states: Default::default(),
83            scope_stack: Default::default(),
84            suspense_stack: Default::default(),
85            current_task: Default::default(),
86            tasks: Default::default(),
87            suspended_tasks: Default::default(),
88            pending_effects: Default::default(),
89            dirty_tasks: Default::default(),
90            elements: RefCell::new(elements),
91            mounts: Default::default(),
92        })
93    }
94
95    /// Get the current runtime
96    pub fn current() -> Rc<Self> {
97        RUNTIMES
98            .with(|stack| stack.borrow().last().cloned())
99            .unwrap_or_else(|| {
100                panic!(
101                    "Must be called from inside a Dioxus runtime.
102
103Help: Some APIs in dioxus require a global runtime to be present.
104If you are calling one of these APIs from outside of a dioxus runtime
105(typically in a web-sys closure or dynamic library), you will need to
106grab the runtime from a scope that has it and then move it into your
107new scope with a runtime guard.
108
109For example, if you are trying to use dioxus apis from a web-sys
110closure, you can grab the runtime from the scope it is created in:
111
112```rust
113use dioxus::prelude::*;
114static COUNT: GlobalSignal<i32> = Signal::global(|| 0);
115
116#[component]
117fn MyComponent() -> Element {{
118    use_effect(|| {{
119        // Grab the runtime from the MyComponent scope
120        let runtime = Runtime::current().expect(\"Components run in the Dioxus runtime\");
121        // Move the runtime into the web-sys closure scope
122        let web_sys_closure = Closure::new(|| {{
123            // Then create a guard to provide the runtime to the closure
124            let _guard = RuntimeGuard::new(runtime);
125            // and run whatever code needs the runtime
126            tracing::info!(\"The count is: {{COUNT}}\");
127        }});
128    }})
129}}
130```"
131                )
132            })
133    }
134
135    /// Try to get the current runtime, returning None if it doesn't exist (outside the context of a dioxus app)
136    pub fn try_current() -> Option<Rc<Self>> {
137        RUNTIMES.with(|stack| stack.borrow().last().cloned())
138    }
139
140    /// Wrap a closure so that it always runs in the runtime that is currently active
141    pub fn wrap_closure<'a, I, O>(f: impl Fn(I) -> O + 'a) -> impl Fn(I) -> O + 'a {
142        let current_runtime = Self::current();
143        move |input| match current_runtime.try_current_scope_id() {
144            Some(scope) => current_runtime.in_scope(scope, || f(input)),
145            None => {
146                let _runtime_guard = RuntimeGuard::new(current_runtime.clone());
147                f(input)
148            }
149        }
150    }
151
152    /// Run a closure with the rendering flag set to true
153    pub(crate) fn while_rendering<T>(&self, f: impl FnOnce() -> T) -> T {
154        self.rendering.set(true);
155        let result = f();
156        self.rendering.set(false);
157        result
158    }
159
160    /// Run a closure with the rendering flag set to false
161    pub(crate) fn while_not_rendering<T>(&self, f: impl FnOnce() -> T) -> T {
162        let previous = self.rendering.get();
163        self.rendering.set(false);
164        let result = f();
165        self.rendering.set(previous);
166        result
167    }
168
169    /// Create a scope context. This slab is synchronized with the scope slab.
170    pub(crate) fn create_scope(&self, context: Scope) {
171        let id = context.id;
172        let mut scopes = self.scope_states.borrow_mut();
173        if scopes.len() <= id.0 {
174            scopes.resize_with(id.0 + 1, Default::default);
175        }
176        scopes[id.0] = Some(context);
177    }
178
179    pub(crate) fn remove_scope(self: &Rc<Self>, id: ScopeId) {
180        {
181            let borrow = self.scope_states.borrow();
182            if let Some(scope) = &borrow[id.0] {
183                // Manually drop tasks, hooks, and contexts inside of the runtime
184                self.in_scope(id, || {
185                    // Drop all spawned tasks - order doesn't matter since tasks don't rely on eachother
186                    // In theory nested tasks might not like this
187                    for id in scope.spawned_tasks.take() {
188                        self.remove_task(id);
189                    }
190
191                    // Drop all queued effects
192                    self.pending_effects
193                        .borrow_mut()
194                        .remove(&ScopeOrder::new(scope.height, scope.id));
195
196                    // Drop all hooks in reverse order in case a hook depends on another hook.
197                    for hook in scope.hooks.take().drain(..).rev() {
198                        drop(hook);
199                    }
200
201                    // Drop all contexts
202                    scope.shared_contexts.take();
203                });
204            }
205        }
206        self.scope_states.borrow_mut()[id.0].take();
207    }
208
209    /// Get the owner for the current scope.
210    #[track_caller]
211    pub fn current_owner<S: AnyStorage>(&self) -> Owner<S> {
212        self.get_state(self.current_scope_id()).owner()
213    }
214
215    /// Get the owner for the current scope.
216    #[track_caller]
217    pub fn scope_owner<S: AnyStorage>(&self, scope: ScopeId) -> Owner<S> {
218        self.get_state(scope).owner()
219    }
220
221    /// Get the current scope id
222    pub fn current_scope_id(&self) -> ScopeId {
223        self.scope_stack.borrow().last().copied().unwrap()
224    }
225
226    /// Try to get the current scope id, returning None if it we aren't actively inside a scope
227    pub fn try_current_scope_id(&self) -> Option<ScopeId> {
228        self.scope_stack.borrow().last().copied()
229    }
230
231    /// Call this function with the current scope set to the given scope
232    #[track_caller]
233    pub fn in_scope<O>(self: &Rc<Self>, id: ScopeId, f: impl FnOnce() -> O) -> O {
234        let _runtime_guard = RuntimeGuard::new(self.clone());
235        {
236            self.push_scope(id);
237        }
238        let o = f();
239        {
240            self.pop_scope();
241        }
242        o
243    }
244
245    /// Get the current suspense location
246    pub(crate) fn current_suspense_location(&self) -> Option<SuspenseLocation> {
247        self.suspense_stack.borrow().last().cloned()
248    }
249
250    /// Run a callback a [`SuspenseLocation`] at the top of the stack
251    pub(crate) fn with_suspense_location<O>(
252        &self,
253        suspense_location: SuspenseLocation,
254        f: impl FnOnce() -> O,
255    ) -> O {
256        self.suspense_stack.borrow_mut().push(suspense_location);
257        let o = f();
258        self.suspense_stack.borrow_mut().pop();
259        o
260    }
261
262    /// Run a callback with the current scope at the top of the stack
263    pub(crate) fn with_scope_on_stack<O>(&self, scope: ScopeId, f: impl FnOnce() -> O) -> O {
264        self.push_scope(scope);
265        let o = f();
266        self.pop_scope();
267        o
268    }
269
270    /// Push a scope onto the stack
271    fn push_scope(&self, scope: ScopeId) {
272        let suspense_location = self
273            .scope_states
274            .borrow()
275            .get(scope.0)
276            .and_then(|s| s.as_ref())
277            .map(|s| s.suspense_location())
278            .unwrap_or_default();
279        self.suspense_stack.borrow_mut().push(suspense_location);
280        self.scope_stack.borrow_mut().push(scope);
281    }
282
283    /// Pop a scope off the stack
284    fn pop_scope(&self) {
285        self.scope_stack.borrow_mut().pop();
286        self.suspense_stack.borrow_mut().pop();
287    }
288
289    /// Get the state for any scope given its ID
290    ///
291    /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
292    pub(crate) fn get_state(&self, id: ScopeId) -> Ref<'_, Scope> {
293        Ref::filter_map(self.scope_states.borrow(), |scopes| {
294            scopes.get(id.0).and_then(|f| f.as_ref())
295        })
296        .ok()
297        .unwrap()
298    }
299
300    /// Get the state for any scope given its ID
301    ///
302    /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
303    pub(crate) fn try_get_state(&self, id: ScopeId) -> Option<Ref<'_, Scope>> {
304        Ref::filter_map(self.scope_states.borrow(), |contexts| {
305            contexts.get(id.0).and_then(|f| f.as_ref())
306        })
307        .ok()
308    }
309
310    /// Pushes a new scope onto the stack
311    pub(crate) fn push(runtime: Rc<Runtime>) {
312        RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
313    }
314
315    /// Pops a scope off the stack
316    pub(crate) fn pop() {
317        RUNTIMES.with(|stack| stack.borrow_mut().pop().unwrap());
318    }
319
320    /// Runs a function with the current runtime
321    pub(crate) fn with<R>(callback: impl FnOnce(&Runtime) -> R) -> R {
322        callback(&Self::current())
323    }
324
325    /// Runs a function with the current scope
326    pub(crate) fn with_current_scope<R>(callback: impl FnOnce(&Scope) -> R) -> R {
327        Self::with(|rt| Self::with_scope(rt.current_scope_id(), callback))
328    }
329
330    /// Runs a function with the current scope
331    pub(crate) fn with_scope<R>(scope: ScopeId, callback: impl FnOnce(&Scope) -> R) -> R {
332        let rt = Runtime::current();
333        Self::in_scope(&rt, scope, || callback(&rt.get_state(scope)))
334    }
335
336    /// Finish a render. This will mark all effects as ready to run and send the render signal.
337    pub(crate) fn finish_render(&self) {
338        // If there are new effects we can run, send a message to the scheduler to run them (after the renderer has applied the mutations)
339        if !self.pending_effects.borrow().is_empty() {
340            self.sender
341                .unbounded_send(SchedulerMsg::EffectQueued)
342                .expect("Scheduler should exist");
343        }
344    }
345
346    /// Check if we should render a scope
347    pub(crate) fn scope_should_render(&self, scope_id: ScopeId) -> bool {
348        // If there are no suspended futures, we know the scope is not  and we can skip context checks
349        if self.suspended_tasks.get() == 0 {
350            return true;
351        }
352
353        // If this is not a suspended scope, and we are under a frozen context, then we should
354        let scopes = self.scope_states.borrow();
355        let scope = &scopes[scope_id.0].as_ref().unwrap();
356        !matches!(scope.suspense_location(), SuspenseLocation::UnderSuspense(suspense) if suspense.is_suspended())
357    }
358
359    /// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an element with a listener, not a static node or a text node.**
360    ///
361    /// This method will identify the appropriate element. The data must match up with the listener declared. Note that
362    /// this method does not give any indication as to the success of the listener call. If the listener is not found,
363    /// nothing will happen.
364    ///
365    /// It is up to the listeners themselves to mark nodes as dirty.
366    ///
367    /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline"
368    #[instrument(skip(self, event), level = "trace", name = "Runtime::handle_event")]
369    pub fn handle_event(self: &Rc<Self>, name: &str, event: Event<dyn Any>, element: ElementId) {
370        let _runtime = RuntimeGuard::new(self.clone());
371        let elements = self.elements.borrow();
372
373        if let Some(Some(parent_path)) = elements.get(element.0).copied() {
374            if event.propagates() {
375                self.handle_bubbling_event(parent_path, name, event);
376            } else {
377                self.handle_non_bubbling_event(parent_path, name, event);
378            }
379        }
380    }
381
382    /*
383    ------------------------
384    The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
385    we find the target path.
386
387    With the target path, we try and move up to the parent until there is no parent.
388    Due to how bubbling works, we call the listeners before walking to the parent.
389
390    If we wanted to do capturing, then we would accumulate all the listeners and call them in reverse order.
391    ----------------------
392
393    For a visual demonstration, here we present a tree on the left and whether or not a listener is collected on the
394    right.
395
396    |           <-- yes (is ascendant)
397    | | |       <-- no  (is not direct ascendant)
398    | |         <-- yes (is ascendant)
399    | | | | |   <--- target element, break early, don't check other listeners
400    | | |       <-- no, broke early
401    |           <-- no, broke early
402    */
403    #[instrument(
404        skip(self, uievent),
405        level = "trace",
406        name = "VirtualDom::handle_bubbling_event"
407    )]
408    fn handle_bubbling_event(&self, parent: ElementRef, name: &str, uievent: Event<dyn Any>) {
409        let mounts = self.mounts.borrow();
410
411        // If the event bubbles, we traverse through the tree until we find the target element.
412        // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
413        let mut parent = Some(parent);
414        while let Some(path) = parent {
415            let mut listeners = vec![];
416
417            let Some(mount) = mounts.get(path.mount.0) else {
418                // If the node is suspended and not mounted, we can just ignore the event
419                return;
420            };
421            let el_ref = &mount.node;
422            let node_template = el_ref.template;
423            let target_path = path.path;
424
425            // Accumulate listeners into the listener list bottom to top
426            for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
427                let attrs = &*el_ref.dynamic_attrs[idx];
428
429                for attr in attrs.iter() {
430                    // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
431                    if attr.name.get(2..) == Some(name) && target_path.is_descendant(this_path) {
432                        listeners.push(&attr.value);
433
434                        // Break if this is the exact target element.
435                        // This means we won't call two listeners with the same name on the same element. This should be
436                        // documented, or be rejected from the rsx! macro outright
437                        if target_path == this_path {
438                            break;
439                        }
440                    }
441                }
442            }
443
444            // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
445            // We check the bubble state between each call to see if the event has been stopped from bubbling
446            tracing::event!(
447                tracing::Level::TRACE,
448                "Calling {} listeners",
449                listeners.len()
450            );
451            for listener in listeners.into_iter().rev() {
452                if let AttributeValue::Listener(listener) = listener {
453                    listener.call(uievent.clone());
454                    let metadata = uievent.metadata.borrow();
455
456                    if !metadata.propagates {
457                        return;
458                    }
459                }
460            }
461
462            let mount = el_ref.mount.get().as_usize();
463            parent = mount.and_then(|id| mounts.get(id).and_then(|el| el.parent));
464        }
465    }
466
467    /// Call an event listener in the simplest way possible without bubbling upwards
468    #[instrument(
469        skip(self, uievent),
470        level = "trace",
471        name = "VirtualDom::handle_non_bubbling_event"
472    )]
473    fn handle_non_bubbling_event(&self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
474        let mounts = self.mounts.borrow();
475        let Some(mount) = mounts.get(node.mount.0) else {
476            // If the node is suspended and not mounted, we can just ignore the event
477            return;
478        };
479        let el_ref = &mount.node;
480        let node_template = el_ref.template;
481        let target_path = node.path;
482
483        for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
484            let attrs = &*el_ref.dynamic_attrs[idx];
485
486            for attr in attrs.iter() {
487                // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
488                // Only call the listener if this is the exact target element.
489                if attr.name.get(2..) == Some(name) && target_path == this_path {
490                    if let AttributeValue::Listener(listener) = &attr.value {
491                        listener.call(uievent.clone());
492                        break;
493                    }
494                }
495            }
496        }
497    }
498
499    /// Consume context from the current scope
500    pub fn consume_context<T: 'static + Clone>(&self, id: ScopeId) -> Option<T> {
501        self.get_state(id).consume_context::<T>()
502    }
503
504    /// Consume context from the current scope
505    pub fn consume_context_from_scope<T: 'static + Clone>(&self, scope_id: ScopeId) -> Option<T> {
506        self.get_state(scope_id).consume_context::<T>()
507    }
508
509    /// Check if the current scope has a context
510    pub fn has_context<T: 'static + Clone>(&self, id: ScopeId) -> Option<T> {
511        self.get_state(id).has_context::<T>()
512    }
513
514    /// Provide context to the current scope
515    pub fn provide_context<T: 'static + Clone>(&self, id: ScopeId, value: T) -> T {
516        self.get_state(id).provide_context(value)
517    }
518
519    /// Get the parent of the current scope if it exists
520    pub fn parent_scope(&self, scope: ScopeId) -> Option<ScopeId> {
521        self.get_state(scope).parent_id()
522    }
523
524    /// Check if the current scope is a descendant of the given scope
525    pub fn is_descendant_of(&self, us: ScopeId, other: ScopeId) -> bool {
526        let mut current = us;
527        while let Some(parent) = self.parent_scope(current) {
528            if parent == other {
529                return true;
530            }
531            current = parent;
532        }
533        false
534    }
535
536    /// Mark the current scope as dirty, causing it to re-render
537    pub fn needs_update(&self, scope: ScopeId) {
538        self.get_state(scope).needs_update();
539    }
540
541    /// Get the height of the current scope
542    pub fn height(&self, id: ScopeId) -> u32 {
543        self.get_state(id).height
544    }
545
546    /// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`](crate::ErrorBoundary) or the root of the app.
547    ///
548    /// # Examples
549    /// ```rust, no_run
550    /// # use dioxus::prelude::*;
551    /// fn Component() -> Element {
552    ///     let request = spawn(async move {
553    ///         match reqwest::get("https://api.example.com").await {
554    ///             Ok(_) => unimplemented!(),
555    ///             // You can explicitly throw an error into a scope with throw_error
556    ///             Err(err) => dioxus::core::Runtime::current().throw_error(ScopeId::APP, err),
557    ///         }
558    ///     });
559    ///
560    ///     unimplemented!()
561    /// }
562    /// ```
563    pub fn throw_error(&self, id: ScopeId, error: impl Into<CapturedError> + 'static) {
564        let error = error.into();
565        if let Some(cx) = self.consume_context::<crate::ErrorContext>(id) {
566            cx.insert_error(error)
567        } else {
568            tracing::error!(
569                    "Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}",
570                    error
571                )
572        }
573    }
574
575    /// Get the suspense context the current scope is in
576    pub fn suspense_context(&self) -> Option<SuspenseContext> {
577        self.get_state(self.current_scope_id())
578            .suspense_location()
579            .suspense_context()
580            .cloned()
581    }
582
583    /// Force every component to be dirty and require a re-render. Used by hot-reloading.
584    ///
585    /// This might need to change to a different flag in the event hooks order changes within components.
586    /// What we really need is a way to mark components as needing a complete rebuild if they were hit by changes.
587    pub fn force_all_dirty(&self) {
588        self.scope_states.borrow_mut().iter().for_each(|state| {
589            if let Some(scope) = state.as_ref() {
590                scope.needs_update();
591            }
592        });
593    }
594
595    /// Check if the virtual dom is currently rendering
596    pub fn vdom_is_rendering(&self) -> bool {
597        self.rendering.get()
598    }
599}
600
601/// A guard for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime.
602///
603/// ```rust
604/// use dioxus::prelude::*;
605/// use dioxus_core::{Runtime, RuntimeGuard};
606///
607/// fn main() {
608///     let virtual_dom = VirtualDom::new(app);
609/// }
610///
611/// fn app() -> Element {
612///     rsx! { Component { runtime: Runtime::current() } }
613/// }
614///
615/// // In a dynamic library
616/// #[derive(Props, Clone)]
617/// struct ComponentProps {
618///    runtime: std::rc::Rc<Runtime>,
619/// }
620///
621/// impl PartialEq for ComponentProps {
622///     fn eq(&self, _other: &Self) -> bool {
623///         true
624///     }
625/// }
626///
627/// fn Component(cx: ComponentProps) -> Element {
628///     use_hook(|| {
629///         let _guard = RuntimeGuard::new(cx.runtime.clone());
630///     });
631///
632///     rsx! { div {} }
633/// }
634/// ```
635pub struct RuntimeGuard(());
636
637impl RuntimeGuard {
638    /// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped
639    pub fn new(runtime: Rc<Runtime>) -> Self {
640        Runtime::push(runtime);
641        Self(())
642    }
643}
644
645impl Drop for RuntimeGuard {
646    fn drop(&mut self) {
647        Runtime::pop();
648    }
649}