Skip to main content

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        // If the event bubbles, we traverse through the tree until we find the target element.
410        // Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
411        let mut parent = Some(parent);
412        while let Some(path) = parent {
413            let mut listeners = vec![];
414            let mount_id;
415
416            // We do this in its own block to prevent mounts from staying open while we call user code
417            {
418                let mounts = self.mounts.borrow();
419                let Some(mount) = mounts.get(path.mount.0) else {
420                    // If the node is suspended and not mounted, we can just ignore the event
421                    return;
422                };
423
424                let el_ref = &mount.node;
425                let node_template = el_ref.template;
426                let target_path = path.path;
427                mount_id = el_ref.mount.get().as_usize();
428
429                // Accumulate listeners into the listener list bottom to top
430                for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
431                    let attrs = &*el_ref.dynamic_attrs[idx];
432
433                    for attr in attrs.iter() {
434                        // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
435                        if attr.name.get(2..) == Some(name) && target_path.is_descendant(this_path)
436                        {
437                            if let AttributeValue::Listener(listener) = &attr.value {
438                                listeners.push(listener.clone());
439                            }
440
441                            // Break if this is the exact target element.
442                            // This means we won't call two listeners with the same name on the same element. This should be
443                            // documented, or be rejected from the rsx! macro outright
444                            if target_path == this_path {
445                                break;
446                            }
447                        }
448                    }
449                }
450            }
451
452            // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
453            // We check the bubble state between each call to see if the event has been stopped from bubbling
454            tracing::event!(
455                tracing::Level::TRACE,
456                "Calling {} listeners",
457                listeners.len()
458            );
459            for listener in listeners.into_iter().rev() {
460                listener.call(uievent.clone());
461                let metadata = uievent.metadata.borrow();
462
463                if !metadata.propagates {
464                    return;
465                }
466            }
467
468            parent = mount_id.and_then(|id| self.mounts.borrow().get(id).and_then(|el| el.parent));
469        }
470    }
471
472    /// Call an event listener in the simplest way possible without bubbling upwards
473    #[instrument(
474        skip(self, uievent),
475        level = "trace",
476        name = "VirtualDom::handle_non_bubbling_event"
477    )]
478    fn handle_non_bubbling_event(&self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
479        let mounts = self.mounts.borrow();
480        let Some(mount) = mounts.get(node.mount.0) else {
481            // If the node is suspended and not mounted, we can just ignore the event
482            return;
483        };
484        let el_ref = &mount.node;
485        let node_template = el_ref.template;
486        let target_path = node.path;
487
488        for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
489            let attrs = &*el_ref.dynamic_attrs[idx];
490
491            for attr in attrs.iter() {
492                // Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
493                // Only call the listener if this is the exact target element.
494                if attr.name.get(2..) == Some(name) && target_path == this_path {
495                    if let AttributeValue::Listener(listener) = &attr.value {
496                        listener.call(uievent.clone());
497                        break;
498                    }
499                }
500            }
501        }
502    }
503
504    /// Consume context from the current scope
505    pub fn consume_context<T: 'static + Clone>(&self, id: ScopeId) -> Option<T> {
506        self.get_state(id).consume_context::<T>()
507    }
508
509    /// Consume context from the current scope
510    pub fn consume_context_from_scope<T: 'static + Clone>(&self, scope_id: ScopeId) -> Option<T> {
511        self.get_state(scope_id).consume_context::<T>()
512    }
513
514    /// Check if the current scope has a context
515    pub fn has_context<T: 'static + Clone>(&self, id: ScopeId) -> Option<T> {
516        self.get_state(id).has_context::<T>()
517    }
518
519    /// Provide context to the current scope
520    pub fn provide_context<T: 'static + Clone>(&self, id: ScopeId, value: T) -> T {
521        self.get_state(id).provide_context(value)
522    }
523
524    /// Get the parent of the current scope if it exists
525    pub fn parent_scope(&self, scope: ScopeId) -> Option<ScopeId> {
526        self.get_state(scope).parent_id()
527    }
528
529    /// Check if the current scope is a descendant of the given scope
530    pub fn is_descendant_of(&self, us: ScopeId, other: ScopeId) -> bool {
531        let mut current = us;
532        while let Some(parent) = self.parent_scope(current) {
533            if parent == other {
534                return true;
535            }
536            current = parent;
537        }
538        false
539    }
540
541    /// Mark the current scope as dirty, causing it to re-render
542    pub fn needs_update(&self, scope: ScopeId) {
543        self.get_state(scope).needs_update();
544    }
545
546    /// Get the height of the current scope
547    pub fn height(&self, id: ScopeId) -> u32 {
548        self.get_state(id).height
549    }
550
551    /// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`](crate::ErrorBoundary) or the root of the app.
552    ///
553    /// # Examples
554    /// ```rust, no_run
555    /// # use dioxus::prelude::*;
556    /// fn Component() -> Element {
557    ///     let request = spawn(async move {
558    ///         match reqwest::get("https://api.example.com").await {
559    ///             Ok(_) => unimplemented!(),
560    ///             // You can explicitly throw an error into a scope with throw_error
561    ///             Err(err) => dioxus::core::Runtime::current().throw_error(ScopeId::APP, err),
562    ///         }
563    ///     });
564    ///
565    ///     unimplemented!()
566    /// }
567    /// ```
568    pub fn throw_error(&self, id: ScopeId, error: impl Into<CapturedError> + 'static) {
569        let error = error.into();
570        if let Some(cx) = self.consume_context::<crate::ErrorContext>(id) {
571            cx.insert_error(error)
572        } else {
573            tracing::error!(
574                    "Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}",
575                    error
576                )
577        }
578    }
579
580    /// Get the suspense context the current scope is in
581    pub fn suspense_context(&self) -> Option<SuspenseContext> {
582        self.get_state(self.current_scope_id())
583            .suspense_location()
584            .suspense_context()
585            .cloned()
586    }
587
588    /// Force every component to be dirty and require a re-render. Used by hot-reloading.
589    ///
590    /// This might need to change to a different flag in the event hooks order changes within components.
591    /// What we really need is a way to mark components as needing a complete rebuild if they were hit by changes.
592    pub fn force_all_dirty(&self) {
593        self.scope_states.borrow_mut().iter().for_each(|state| {
594            if let Some(scope) = state.as_ref() {
595                scope.needs_update();
596            }
597        });
598    }
599
600    /// Check if the virtual dom is currently rendering
601    pub fn vdom_is_rendering(&self) -> bool {
602        self.rendering.get()
603    }
604}
605
606/// 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.
607///
608/// ```rust
609/// use dioxus::prelude::*;
610/// use dioxus_core::{Runtime, RuntimeGuard};
611///
612/// fn main() {
613///     let virtual_dom = VirtualDom::new(app);
614/// }
615///
616/// fn app() -> Element {
617///     rsx! { Component { runtime: Runtime::current() } }
618/// }
619///
620/// // In a dynamic library
621/// #[derive(Props, Clone)]
622/// struct ComponentProps {
623///    runtime: std::rc::Rc<Runtime>,
624/// }
625///
626/// impl PartialEq for ComponentProps {
627///     fn eq(&self, _other: &Self) -> bool {
628///         true
629///     }
630/// }
631///
632/// fn Component(cx: ComponentProps) -> Element {
633///     use_hook(|| {
634///         let _guard = RuntimeGuard::new(cx.runtime.clone());
635///     });
636///
637///     rsx! { div {} }
638/// }
639/// ```
640pub struct RuntimeGuard(());
641
642impl RuntimeGuard {
643    /// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped
644    pub fn new(runtime: Rc<Runtime>) -> Self {
645        Runtime::push(runtime);
646        Self(())
647    }
648}
649
650impl Drop for RuntimeGuard {
651    fn drop(&mut self) {
652        Runtime::pop();
653    }
654}