diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 7c355c260..b5d879eef 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -167,7 +167,7 @@ where error.context.push(Rc::new(AdditionalErrorContext { backtrace: Backtrace::capture(), context: Box::new(context()), - scope: current_scope_id(), + scope: current_scope_id().ok(), })); Err(error) } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 0803ce41b..b3efdbbf2 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -397,7 +397,7 @@ impl Callback { )); Self { callback, - origin: current_scope_id().expect("to be in a dioxus runtime"), + origin: current_scope_id().unwrap(), } } @@ -409,7 +409,7 @@ impl Callback { as Rc Ret>>)); Self { callback, - origin: current_scope_id().expect("to be in a dioxus runtime"), + origin: current_scope_id().unwrap(), } } @@ -427,7 +427,7 @@ impl Callback { value }) }) - .expect("Callback must be called from within the dioxus runtime") + .unwrap() } else { panic!("Callback was manually dropped") } diff --git a/packages/core/src/generational_box.rs b/packages/core/src/generational_box.rs index 5a5c485b2..0d96afb76 100644 --- a/packages/core/src/generational_box.rs +++ b/packages/core/src/generational_box.rs @@ -91,6 +91,6 @@ pub fn current_owner() -> Owner { impl ScopeId { /// Get the owner for the current scope. pub fn owner(self) -> Owner { - Runtime::with_scope(self, |cx| cx.owner::()).expect("to be in a dioxus runtime") + Runtime::with_scope(self, |cx| cx.owner::()).unwrap() } } diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index c167f3cf3..cf3b658e8 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -1,10 +1,14 @@ +use crate::runtime::RuntimeError; use crate::{innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, Task}; use std::future::Future; use std::sync::Arc; /// Get the current scope id -pub fn current_scope_id() -> Option { - Runtime::with(|rt| rt.current_scope_id()).flatten() +pub fn current_scope_id() -> Result { + Runtime::with(|rt| rt.current_scope_id().ok()) + .ok() + .flatten() + .ok_or(RuntimeError::new()) } #[doc(hidden)] @@ -31,19 +35,20 @@ pub fn vdom_is_rendering() -> bool { /// } /// ``` pub fn throw_error(error: impl Into + 'static) { - current_scope_id() - .expect("to be in a dioxus runtime") - .throw_error(error) + current_scope_id().unwrap().throw_error(error) } /// Consume context from the current scope pub fn try_consume_context() -> Option { - Runtime::with_current_scope(|cx| cx.consume_context::()).flatten() + Runtime::with_current_scope(|cx| cx.consume_context::()) + .ok() + .flatten() } /// Consume context from the current scope pub fn consume_context() -> T { Runtime::with_current_scope(|cx| cx.consume_context::()) + .ok() .flatten() .unwrap_or_else(|| panic!("Could not find context {}", std::any::type_name::())) } @@ -54,23 +59,25 @@ pub fn consume_context_from_scope(scope_id: ScopeId) -> Opti rt.get_state(scope_id) .and_then(|cx| cx.consume_context::()) }) + .ok() .flatten() } /// Check if the current scope has a context pub fn has_context() -> Option { - Runtime::with_current_scope(|cx| cx.has_context::()).flatten() + Runtime::with_current_scope(|cx| cx.has_context::()) + .ok() + .flatten() } /// Provide context to the current scope pub fn provide_context(value: T) -> T { - Runtime::with_current_scope(|cx| cx.provide_context(value)).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.provide_context(value)).unwrap() } /// Provide a context to the root scope pub fn provide_root_context(value: T) -> T { - Runtime::with_current_scope(|cx| cx.provide_root_context(value)) - .expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.provide_root_context(value)).unwrap() } /// Suspended the current component on a specific task and then return None @@ -108,7 +115,7 @@ pub fn suspend(task: Task) -> Element { /// #[doc = include_str!("../docs/common_spawn_errors.md")] pub fn spawn_isomorphic(fut: impl Future + 'static) -> Task { - Runtime::with_current_scope(|cx| cx.spawn_isomorphic(fut)).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.spawn_isomorphic(fut)).unwrap() } /// Spawns the future but does not return the [`Task`]. This task will automatically be canceled when the component is dropped. @@ -134,12 +141,12 @@ pub fn spawn_isomorphic(fut: impl Future + 'static) -> Task { /// #[doc = include_str!("../docs/common_spawn_errors.md")] pub fn spawn(fut: impl Future + 'static) -> Task { - Runtime::with_current_scope(|cx| cx.spawn(fut)).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.spawn(fut)).unwrap() } /// Queue an effect to run after the next render. You generally shouldn't need to interact with this function directly. [use_effect](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) will call this function for you. pub fn queue_effect(f: impl FnOnce() + 'static) { - Runtime::with_current_scope(|cx| cx.queue_effect(f)).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.queue_effect(f)).unwrap() } /// Spawn a future that Dioxus won't clean up when this component is unmounted @@ -195,7 +202,7 @@ pub fn queue_effect(f: impl FnOnce() + 'static) { /// #[doc = include_str!("../docs/common_spawn_errors.md")] pub fn spawn_forever(fut: impl Future + 'static) -> Option { - Runtime::with_scope(ScopeId::ROOT, |cx| cx.spawn(fut)) + Runtime::with_scope(ScopeId::ROOT, |cx| cx.spawn(fut)).ok() } /// Informs the scheduler that this task is no longer needed and should be removed. @@ -248,30 +255,33 @@ pub fn remove_future(id: Task) { /// }) /// } /// ``` +#[track_caller] pub fn use_hook(initializer: impl FnOnce() -> State) -> State { - Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.use_hook(initializer)).unwrap() } /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc pub fn generation() -> usize { - Runtime::with_current_scope(|cx| cx.generation()).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.generation()).unwrap() } /// Get the parent of the current scope if it exists pub fn parent_scope() -> Option { - Runtime::with_current_scope(|cx| cx.parent_id()).flatten() + Runtime::with_current_scope(|cx| cx.parent_id()) + .ok() + .flatten() } /// Mark the current scope as dirty, causing it to re-render pub fn needs_update() { - Runtime::with_current_scope(|cx| cx.needs_update()); + let _ = Runtime::with_current_scope(|cx| cx.needs_update()); } /// Mark the current scope as dirty, causing it to re-render pub fn needs_update_any(id: ScopeId) { - Runtime::with_current_scope(|cx| cx.needs_update_any(id)); + let _ = Runtime::with_current_scope(|cx| cx.needs_update_any(id)); } /// Schedule an update for the current component @@ -280,7 +290,7 @@ pub fn needs_update_any(id: ScopeId) { /// /// You should prefer [`schedule_update_any`] if you need to update multiple components. pub fn schedule_update() -> Arc { - Runtime::with_current_scope(|cx| cx.schedule_update()).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.schedule_update()).unwrap() } /// Schedule an update for any component given its [`ScopeId`]. @@ -289,7 +299,7 @@ pub fn schedule_update() -> Arc { /// /// Note: Unlike [`needs_update`], the function returned by this method will work outside of the dioxus runtime. pub fn schedule_update_any() -> Arc { - Runtime::with_current_scope(|cx| cx.schedule_update_any()).expect("to be in a dioxus runtime") + Runtime::with_current_scope(|cx| cx.schedule_update_any()).unwrap() } /// Creates a callback that will be run before the component is removed. @@ -393,12 +403,12 @@ pub fn use_after_render(f: impl FnMut() + 'static) { /// This is a hook and will always run, so you can't unschedule it /// Will run for every progression of suspense, though this might change in the future pub fn before_render(f: impl FnMut() + 'static) { - Runtime::with_current_scope(|cx| cx.push_before_render(f)); + let _ = Runtime::with_current_scope(|cx| cx.push_before_render(f)); } /// Push a function to be run after the render is complete, even if it didn't complete successfully pub fn after_render(f: impl FnMut() + 'static) { - Runtime::with_current_scope(|cx| cx.push_after_render(f)); + let _ = Runtime::with_current_scope(|cx| cx.push_after_render(f)); } /// Use a hook with a cleanup function diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index fc2b58331..e419e3864 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -130,9 +130,11 @@ pub fn verify_component_called_as_component, P, M>(co } let component_name = Runtime::with(|rt| { current_scope_id() + .ok() .and_then(|id| rt.get_state(id)) .map(|scope| scope.name) }) + .ok() .flatten(); // If we are in a component, and the type name is the same as the active component name, then we can just return diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index dfe8638ca..73490ef25 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -8,6 +8,7 @@ use crate::{ }; use slotmap::DefaultKey; use std::collections::BTreeSet; +use std::fmt; use std::{ cell::{Cell, Ref, RefCell}, rc::Rc, @@ -66,8 +67,10 @@ impl Runtime { } /// Get the current runtime - pub fn current() -> Option> { - RUNTIMES.with(|stack| stack.borrow().last().cloned()) + pub fn current() -> Result, RuntimeError> { + RUNTIMES + .with(|stack| stack.borrow().last().cloned()) + .ok_or(RuntimeError::new()) } /// Create a scope context. This slab is synchronized with the scope slab. @@ -106,8 +109,12 @@ impl Runtime { } /// Get the current scope id - pub(crate) fn current_scope_id(&self) -> Option { - self.scope_stack.borrow().last().copied() + pub(crate) fn current_scope_id(&self) -> Result { + self.scope_stack + .borrow() + .last() + .copied() + .ok_or(RuntimeError { _priv: () }) } /// Call this function with the current scope set to the given scope @@ -190,22 +197,31 @@ impl Runtime { } /// Runs a function with the current runtime - pub(crate) fn with(f: impl FnOnce(&Runtime) -> R) -> Option { + pub(crate) fn with(f: impl FnOnce(&Runtime) -> R) -> Result { Self::current().map(|r| f(&r)) } /// Runs a function with the current scope - pub(crate) fn with_current_scope(f: impl FnOnce(&Scope) -> R) -> Option { + pub(crate) fn with_current_scope(f: impl FnOnce(&Scope) -> R) -> Result { Self::with(|rt| { rt.current_scope_id() + .ok() .and_then(|scope| rt.get_state(scope).map(|sc| f(&sc))) }) + .ok() .flatten() + .ok_or(RuntimeError::new()) } /// Runs a function with the current scope - pub(crate) fn with_scope(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option { - Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten() + pub(crate) fn with_scope( + scope: ScopeId, + f: impl FnOnce(&Scope) -> R, + ) -> Result { + Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))) + .ok() + .flatten() + .ok_or(RuntimeError::new()) } /// Finish a render. This will mark all effects as ready to run and send the render signal. @@ -279,3 +295,61 @@ impl Drop for RuntimeGuard { Runtime::pop(); } } + +/// Missing Dioxus runtime error. +pub struct RuntimeError { + _priv: (), +} + +impl RuntimeError { + #[inline(always)] + pub(crate) fn new() -> Self { + Self { _priv: () } + } +} + +impl fmt::Debug for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RuntimeError").finish() + } +} + +impl fmt::Display for RuntimeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Must be called from inside a Dioxus runtime. + +Help: Some APIs in dioxus require a global runtime to be present. +If you are calling one of these APIs from outside of a dioxus runtime +(typically in a web-sys closure or dynamic library), you will need to +grab the runtime from a scope that has it and then move it into your +new scope with a runtime guard. + +For example, if you are trying to use dioxus apis from a web-sys +closure, you can grab the runtime from the scope it is created in: + +```rust +use dioxus::prelude::*; +static COUNT: GlobalSignal = Signal::global(|| 0); + +#[component] +fn MyComponent() -> Element { + use_effect(|| { + // Grab the runtime from the MyComponent scope + let runtime = Runtime::current().expect(\"Components run in the Dioxus runtime\"); + // Move the runtime into the web-sys closure scope + let web_sys_closure = Closure::new(|| { + // Then create a guard to provide the runtime to the closure + let _guard = RuntimeGuard::new(runtime); + // and run whatever code needs the runtime + tracing::info!(\"The count is: {COUNT}\"); + }); + }) +} +```" + ) + } +} + +impl std::error::Error for RuntimeError {} diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 80f2c723a..cb236fb00 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -16,7 +16,7 @@ impl VirtualDom { props: BoxedAnyProps, name: &'static str, ) -> &mut ScopeState { - let parent_id = self.runtime.current_scope_id(); + let parent_id = self.runtime.current_scope_id().ok(); let height = match parent_id.and_then(|id| self.runtime.get_state(id)) { Some(parent) => parent.height() + 1, None => 0, @@ -47,11 +47,11 @@ impl VirtualDom { /// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope. #[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")] + #[track_caller] pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn { - debug_assert!( - crate::Runtime::current().is_some(), - "Must be in a dioxus runtime" - ); + // Ensure we are currently inside a `Runtime`. + crate::Runtime::current().unwrap(); + self.runtime.clone().with_scope_on_stack(scope_id, || { let scope = &self.scopes[scope_id.0]; let output = { diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 7589be31c..f695fa19f 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -1,3 +1,4 @@ +use crate::runtime::RuntimeError; use crate::{innerlude::SchedulerMsg, Runtime, ScopeId, Task}; use crate::{ innerlude::{throw_into, CapturedError}, @@ -219,7 +220,7 @@ impl Scope { None }); - match cur_runtime.flatten() { + match cur_runtime.ok().flatten() { Some(ctx) => Some(ctx), None => { tracing::trace!( @@ -450,13 +451,18 @@ impl Scope { impl ScopeId { /// Get the current scope id - pub fn current_scope_id(self) -> Option { - Runtime::with(|rt| rt.current_scope_id()).flatten() + pub fn current_scope_id(self) -> Result { + Runtime::with(|rt| rt.current_scope_id().ok()) + .ok() + .flatten() + .ok_or(RuntimeError::new()) } /// Consume context from the current scope pub fn consume_context(self) -> Option { - Runtime::with_scope(self, |cx| cx.consume_context::()).flatten() + Runtime::with_scope(self, |cx| cx.consume_context::()) + .ok() + .flatten() } /// Consume context from the current scope @@ -465,64 +471,67 @@ impl ScopeId { rt.get_state(scope_id) .and_then(|cx| cx.consume_context::()) }) + .ok() .flatten() } /// Check if the current scope has a context pub fn has_context(self) -> Option { - Runtime::with_scope(self, |cx| cx.has_context::()).flatten() + Runtime::with_scope(self, |cx| cx.has_context::()) + .ok() + .flatten() } /// Provide context to the current scope pub fn provide_context(self, value: T) -> T { - Runtime::with_scope(self, |cx| cx.provide_context(value)) - .expect("to be in a dioxus runtime") + Runtime::with_scope(self, |cx| cx.provide_context(value)).unwrap() } /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(self, fut: impl Future + 'static) -> Option { - Runtime::with_scope(self, |cx| cx.spawn(fut)) + Runtime::with_scope(self, |cx| cx.spawn(fut)).ok() } /// Spawns the future but does not return the [`Task`] pub fn spawn(self, fut: impl Future + 'static) { - Runtime::with_scope(self, |cx| cx.spawn(fut)); + Runtime::with_scope(self, |cx| cx.spawn(fut)).unwrap(); } /// Get the current render since the inception of this component /// /// This can be used as a helpful diagnostic when debugging hooks/renders, etc pub fn generation(self) -> Option { - Runtime::with_scope(self, |cx| Some(cx.generation())).expect("to be in a dioxus runtime") + Runtime::with_scope(self, |cx| Some(cx.generation())).unwrap() } /// Get the parent of the current scope if it exists pub fn parent_scope(self) -> Option { - Runtime::with_scope(self, |cx| cx.parent_id()).flatten() + Runtime::with_scope(self, |cx| cx.parent_id()) + .ok() + .flatten() } /// Mark the current scope as dirty, causing it to re-render pub fn needs_update(self) { - Runtime::with_scope(self, |cx| cx.needs_update()); + Runtime::with_scope(self, |cx| cx.needs_update()).unwrap(); } /// 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. /// /// ## Notice: you should prefer using [`crate::prelude::schedule_update_any`] pub fn schedule_update(&self) -> Arc { - Runtime::with_scope(*self, |cx| cx.schedule_update()).expect("to be in a dioxus runtime") + Runtime::with_scope(*self, |cx| cx.schedule_update()).unwrap() } /// Get the height of the current scope pub fn height(self) -> u32 { - Runtime::with_scope(self, |cx| cx.height()).expect("to be in a dioxus runtime") + Runtime::with_scope(self, |cx| cx.height()).unwrap() } /// Run a closure inside of scope's runtime + #[track_caller] pub fn in_runtime(self, f: impl FnOnce() -> T) -> T { - Runtime::current() - .expect("to be in a dioxus runtime") - .on_scope(self, f) + Runtime::current().unwrap().on_scope(self, f) } /// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`] or the root of the app. diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index d3f49696a..fca163fc7 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -21,6 +21,7 @@ impl std::fmt::Debug for ScopeId { #[cfg(debug_assertions)] { if let Some(name) = Runtime::current() + .ok() .as_ref() .and_then(|rt| rt.get_state(*self)) { diff --git a/packages/core/src/suspense/mod.rs b/packages/core/src/suspense/mod.rs index 319366b55..474d3d145 100644 --- a/packages/core/src/suspense/mod.rs +++ b/packages/core/src/suspense/mod.rs @@ -46,7 +46,7 @@ impl SuspendedFuture { pub fn new(task: Task) -> Self { Self { task, - origin: current_scope_id().expect("to be in a dioxus runtime"), + origin: current_scope_id().unwrap(), placeholder: VNode::placeholder(), } } diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index 3723476f9..a5d1e7694 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -81,7 +81,8 @@ impl Task { _ = rt .sender .unbounded_send(SchedulerMsg::TaskNotified(self.id)) - }); + }) + .unwrap(); } /// Poll the task immediately. @@ -100,7 +101,8 @@ impl Task { .unbounded_send(SchedulerMsg::TaskNotified(self.id)); } } - }); + }) + .unwrap(); } } @@ -246,7 +248,11 @@ impl Runtime { } pub(crate) fn handle_task_wakeup(&self, id: Task) -> Poll<()> { - debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime"); + #[cfg(feature = "debug_assertions")] + { + // Ensure we are currently inside a `Runtime`. + Runtime::current().unwrap(); + } let task = self.tasks.borrow().get(id.id).cloned(); diff --git a/packages/signals/src/global/signal.rs b/packages/signals/src/global/signal.rs index 914c3449d..3c1b68700 100644 --- a/packages/signals/src/global/signal.rs +++ b/packages/signals/src/global/signal.rs @@ -68,7 +68,7 @@ impl GlobalSignal { #[doc(hidden)] pub fn maybe_with_rt(&self, f: impl FnOnce(&T) -> O) -> O { - if Runtime::current().is_none() { + if Runtime::current().is_err() { f(&(self.initializer)()) } else { self.with(f)