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}