From 14651a35736a35a651c39d0717a639f3c87e9514 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 15 Jan 2024 18:02:58 -0800 Subject: [PATCH] Change context api to panic with nice error message --- examples/compose.rs | 12 ++-- examples/crm.rs | 75 ++++++++++++---------- examples/error_handle.rs | 2 +- examples/spread.rs | 2 +- packages/core/src/error_boundary.rs | 4 +- packages/core/src/global_context.rs | 9 ++- packages/core/src/lib.rs | 4 +- packages/core/src/scheduler/mod.rs | 1 - packages/desktop/src/desktop_context.rs | 2 +- packages/desktop/src/hooks.rs | 2 +- packages/hooks/src/lib.rs | 4 +- packages/hooks/src/use_context.rs | 23 +++++-- packages/hooks/src/use_coroutine.rs | 6 +- packages/hooks/src/use_root_context.rs | 4 +- packages/html/src/eval.rs | 5 +- packages/router/src/contexts/navigator.rs | 2 +- packages/router/src/contexts/outlet.rs | 2 +- packages/router/src/hooks/use_navigator.rs | 4 +- packages/router/src/hooks/use_router.rs | 2 +- 19 files changed, 96 insertions(+), 69 deletions(-) diff --git a/examples/compose.rs b/examples/compose.rs index c725a8ceb..6ddda19dc 100644 --- a/examples/compose.rs +++ b/examples/compose.rs @@ -1,8 +1,9 @@ //! This example shows how to create a popup window and send data back to the parent window. +use std::rc::Rc; + use dioxus::prelude::*; use futures_util::StreamExt; -use tokio::sync::mpsc::UnboundedSender; fn main() { dioxus_desktop::launch(app); @@ -12,15 +13,16 @@ fn app() -> Element { let emails_sent = use_signal(|| Vec::new() as Vec); // Wait for responses to the compose channel, and then push them to the emails_sent signal. - let tx = use_coroutine(|mut rx: UnboundedReceiver| async move { + let handle = use_coroutine(|mut rx: UnboundedReceiver| async move { while let Some(message) = rx.next().await { emails_sent.write().push(message); } }); let open_compose_window = move |evt: MouseEvent| { + let tx = handle.tx(); dioxus_desktop::window().new_window( - VirtualDom::new_with_props(compose, tx.clone()), + VirtualDom::new_with_props(compose, Rc::new(move |s| tx.unbounded_send(s))), Default::default(), ); }; @@ -39,7 +41,7 @@ fn app() -> Element { } } -fn compose(tx: UnboundedSender) -> Element { +fn compose(send: Rc) -> Element { let user_input = use_signal(String::new); rsx! { @@ -48,7 +50,7 @@ fn compose(tx: UnboundedSender) -> Element { button { onclick: move |_| { - tx.send(user_input.get().clone()); + send(user_input.get().clone()); dioxus_desktop::window().close(); }, "Click to send" diff --git a/examples/crm.rs b/examples/crm.rs index ff4694bab..c352f8e98 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -3,29 +3,17 @@ use dioxus::prelude::*; use dioxus_router::prelude::*; fn main() { - dioxus_desktop::launch(App); + dioxus_desktop::launch(app); } -#[derive(Routable, Clone)] -#[rustfmt::skip] -enum Route { - #[route("/")] - ClientList {}, - #[route("/new")] - ClientAdd {}, - #[route("/settings")] - Settings {}, -} +/// A type alias that reprsents a shared context between components +/// +/// Normally we'd wrap the Context in a newtype, but we only have one Signal> in this app +type Clients = Signal>; -#[derive(Clone, Debug, Default)] -pub struct Client { - pub first_name: String, - pub last_name: String, - pub description: String, -} +fn app() -> Element { + use_context_provider::(|| Signal::new(vec![])); -#[component] -fn App() -> Element { render! { link { rel: "stylesheet", @@ -48,8 +36,30 @@ fn App() -> Element { } } +#[derive(Routable, Clone)] +#[rustfmt::skip] +enum Route { + #[route("/")] + ClientList {}, + + #[route("/new")] + ClientAdd {}, + + #[route("/settings")] + Settings {}, +} + +#[derive(Clone, Debug, Default)] +pub struct Client { + pub first_name: String, + pub last_name: String, + pub description: String, +} + #[component] fn ClientList() -> Element { + let clients = use_context::(); + rsx! { h2 { "List of Clients" } Link { to: Route::ClientAdd {}, class: "pure-button pure-button-primary", "Add Client" } @@ -69,21 +79,18 @@ fn ClientAdd() -> Element { let last_name = use_signal(String::new); let description = use_signal(String::new); + let submit_client = move |_: FormEvent| { + consume_context::().write().push(Client { + first_name: first_name.to_string(), + last_name: last_name.to_string(), + description: description.to_string(), + }); + dioxus_router::router().push(Route::ClientList {}); + }; + rsx! { h2 { "Add new Client" } - - form { - class: "pure-form pure-form-aligned", - onsubmit: move |_| { - let mut clients = clients.write(); - clients - .push(Client { - first_name: first_name.to_string(), - last_name: last_name.to_string(), - description: description.to_string(), - }); - dioxus_router::router().push(Route::ClientList {}); - }, + form { class: "pure-form pure-form-aligned", onsubmit: submit_client, fieldset { div { class: "pure-control-group", @@ -131,14 +138,12 @@ fn ClientAdd() -> Element { #[component] fn Settings() -> Element { - let clients = use_shared_state::().unwrap(); - rsx! { h2 { "Settings" } button { class: "pure-button pure-button-primary red", - onclick: move |_| clients.write().clear(), + onclick: move |_| consume_context::().write().clear(), "Remove all Clients" } diff --git a/examples/error_handle.rs b/examples/error_handle.rs index 6f5b6efb5..df2dd06a1 100644 --- a/examples/error_handle.rs +++ b/examples/error_handle.rs @@ -1,4 +1,4 @@ -use dioxus::{core::CapturedError, prelude::*}; +use dioxus::{dioxus_core::CapturedError, prelude::*}; fn main() { dioxus_desktop::launch(app); diff --git a/examples/spread.rs b/examples/spread.rs index f4f80c896..ed204d54b 100644 --- a/examples/spread.rs +++ b/examples/spread.rs @@ -1,4 +1,4 @@ -use dioxus::{core::NoOpMutations, prelude::*}; +use dioxus::{dioxus_core::NoOpMutations, prelude::*}; fn main() { let mut dom = VirtualDom::new(app); diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 7c1e27a22..bd9855ab1 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,5 +1,5 @@ use crate::{ - global_context::{consume_context, current_scope_id}, + global_context::{current_scope_id, try_consume_context}, innerlude::provide_context, use_hook, Element, IntoDynNode, Properties, ScopeId, Template, TemplateAttribute, TemplateNode, VNode, @@ -202,7 +202,7 @@ pub trait Throw: Sized { } fn throw_error(e: impl Debug + 'static) -> Option { - if let Some(cx) = consume_context::() { + if let Some(cx) = try_consume_context::() { match current_scope_id() { Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()), None => { diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index 62f33816d..8abe9ecfb 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -19,10 +19,17 @@ pub fn vdom_is_rendering() -> bool { } /// Consume context from the current scope -pub fn consume_context() -> Option { +pub fn try_consume_context() -> Option { with_current_scope(|cx| cx.consume_context::()).flatten() } +/// Consume context from the current scope +pub fn consume_context() -> T { + with_current_scope(|cx| cx.consume_context::()) + .flatten() + .unwrap_or_else(|| panic!("Could not find context {}", std::any::type_name::(),)) +} + /// Consume context from the current scope pub fn consume_context_from_scope(scope_id: ScopeId) -> Option { with_runtime(|rt| { diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index a9f31df86..3926930e3 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -88,8 +88,8 @@ pub mod prelude { consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, generation, has_context, needs_update, parent_scope, provide_context, provide_root_context, push_future, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, - suspend, use_error_boundary, use_hook, AnyValue, Attribute, Component, Element, - ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, + suspend, try_consume_context, use_error_boundary, use_hook, AnyValue, Attribute, Component, + Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, Properties, Runtime, RuntimeGuard, ScopeId, ScopeState, Task, Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom, }; diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index c6cd0681d..03e419969 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,5 +1,4 @@ use crate::ScopeId; -use slab::Slab; mod task; mod wait; diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 690f3f3e1..b0f960328 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -33,7 +33,7 @@ use tao::platform::ios::WindowExtIOS; /// /// This function will panic if it is called outside of the context of a Dioxus App. pub fn window() -> DesktopContext { - dioxus_core::prelude::consume_context().unwrap() + dioxus_core::prelude::consume_context() } /// A handle to the [`DesktopService`] that can be passed around. diff --git a/packages/desktop/src/hooks.rs b/packages/desktop/src/hooks.rs index 5080d2eb4..60bf4c736 100644 --- a/packages/desktop/src/hooks.rs +++ b/packages/desktop/src/hooks.rs @@ -14,7 +14,7 @@ use wry::RequestAsyncResponder; /// Get an imperative handle to the current window pub fn use_window() -> DesktopContext { - use_hook(|| consume_context::()).unwrap() + use_hook(|| consume_context::()) } /// Get a closure that executes any JavaScript in the WebView context. diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 60e4a48f5..f8f36ce24 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -58,8 +58,8 @@ macro_rules! to_owned { mod use_on_destroy; pub use use_on_destroy::*; -// mod use_context; -// pub use use_context::*; +mod use_context; +pub use use_context::*; mod use_coroutine; pub use use_coroutine::*; diff --git a/packages/hooks/src/use_context.rs b/packages/hooks/src/use_context.rs index d2ecb910c..ac3bb62ea 100644 --- a/packages/hooks/src/use_context.rs +++ b/packages/hooks/src/use_context.rs @@ -1,20 +1,31 @@ -use dioxus_core::ScopeState; +use dioxus_core::{ + prelude::{consume_context, provide_context, try_consume_context}, + use_hook, +}; /// Consume some context in the tree, providing a sharable handle to the value /// /// Does not regenerate the value if the value is changed at the parent. #[must_use] -pub fn use_context() -> Option<&T> { - cx.use_hook(|| cx.consume_context::()).as_ref() +pub fn try_use_context() -> Option { + use_hook(|| try_consume_context::()) +} + +/// Consume some context in the tree, providing a sharable handle to the value +/// +/// Does not regenerate the value if the value is changed at the parent. +#[must_use] +pub fn use_context() -> T { + use_hook(|| consume_context::()) } /// Provide some context via the tree and return a reference to it /// /// Once the context has been provided, it is immutable. Mutations should be done via interior mutability. -pub fn use_context_provider(f: impl FnOnce() -> T) -> &T { - cx.use_hook(|| { +pub fn use_context_provider(f: impl FnOnce() -> T) -> T { + use_hook(|| { let val = f(); - cx.provide_context(val.clone()); + provide_context(val.clone()); val }) } diff --git a/packages/hooks/src/use_coroutine.rs b/packages/hooks/src/use_coroutine.rs index 1c391744c..31f10897e 100644 --- a/packages/hooks/src/use_coroutine.rs +++ b/packages/hooks/src/use_coroutine.rs @@ -96,7 +96,7 @@ where /// /// See the docs for [`use_coroutine`] for more details. #[must_use] -pub fn use_coroutine_handle() -> Option> { +pub fn use_coroutine_handle() -> Coroutine { use_hook(|| consume_context::>()) } @@ -118,6 +118,10 @@ impl Coroutine { let _ = self.tx.read().as_ref().unwrap().unbounded_send(msg); } + pub fn tx(&self) -> UnboundedSender { + self.tx.read().as_ref().unwrap().clone() + } + /// Restart this coroutine /// /// Forces the component to re-render, which will re-invoke the coroutine. diff --git a/packages/hooks/src/use_root_context.rs b/packages/hooks/src/use_root_context.rs index 20e784a5d..a4bc62bb6 100644 --- a/packages/hooks/src/use_root_context.rs +++ b/packages/hooks/src/use_root_context.rs @@ -1,9 +1,9 @@ -use dioxus_core::{prelude::consume_context, prelude::provide_root_context, use_hook}; +use dioxus_core::{prelude::provide_root_context, prelude::try_consume_context, use_hook}; /// pub fn use_root_context(new: impl FnOnce() -> T) -> T { use_hook(|| { - consume_context::() + try_consume_context::() // If no context is provided, create a new one at the root .unwrap_or_else(|| provide_root_context(new()).expect(" A runtime to exist")) }) diff --git a/packages/html/src/eval.rs b/packages/html/src/eval.rs index 7c8433baa..fe5d51383 100644 --- a/packages/html/src/eval.rs +++ b/packages/html/src/eval.rs @@ -35,7 +35,7 @@ type EvalCreator = Rc Result>; /// has access to most, if not all of your application data.** #[must_use] pub fn eval_provider() -> EvalCreator { - let eval_provider = consume_context::>().expect("evaluator not provided"); + let eval_provider = consume_context::>(); Rc::new(move |script: &str| { eval_provider @@ -45,8 +45,7 @@ pub fn eval_provider() -> EvalCreator { } pub fn eval(script: &str) -> Result { - let eval_provider = dioxus_core::prelude::consume_context::>() - .expect("evaluator not provided"); + let eval_provider = dioxus_core::prelude::consume_context::>(); eval_provider .new_evaluator(script.to_string()) diff --git a/packages/router/src/contexts/navigator.rs b/packages/router/src/contexts/navigator.rs index 0542bad1d..962da6c95 100644 --- a/packages/router/src/contexts/navigator.rs +++ b/packages/router/src/contexts/navigator.rs @@ -9,7 +9,7 @@ use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext}; /// Panics if there is no router present. pub fn navigator() -> Navigator { Navigator( - dioxus_lib::prelude::consume_context::() + dioxus_lib::prelude::try_consume_context::() .expect("A router must be present to use navigator"), ) } diff --git a/packages/router/src/contexts/outlet.rs b/packages/router/src/contexts/outlet.rs index 3303bb81c..f809527c0 100644 --- a/packages/router/src/contexts/outlet.rs +++ b/packages/router/src/contexts/outlet.rs @@ -18,7 +18,7 @@ impl Clone for OutletContext { pub(crate) fn use_outlet_context() -> OutletContext { use_hook(|| { - consume_context().unwrap_or(OutletContext:: { + try_consume_context().unwrap_or(OutletContext:: { current_level: 1, _marker: std::marker::PhantomData, }) diff --git a/packages/router/src/hooks/use_navigator.rs b/packages/router/src/hooks/use_navigator.rs index ad20fa72e..e7f2a53a7 100644 --- a/packages/router/src/hooks/use_navigator.rs +++ b/packages/router/src/hooks/use_navigator.rs @@ -1,4 +1,4 @@ -use dioxus_lib::prelude::{consume_context, use_hook}; +use dioxus_lib::prelude::{consume_context, try_consume_context, use_hook}; use crate::prelude::{Navigator, RouterContext}; @@ -51,7 +51,7 @@ use crate::prelude::{Navigator, RouterContext}; #[must_use] pub fn use_navigator() -> Navigator { use_hook(|| { - let router = consume_context::() + let router = try_consume_context::() .expect("Must be called in a descendant of a Router component"); Navigator(router) diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index d94abc7fd..543908d8a 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -9,5 +9,5 @@ pub fn use_router() -> RouterContext { /// Aquire the router without subscribing to updates. pub fn router() -> RouterContext { - dioxus_lib::prelude::consume_context().unwrap() + dioxus_lib::prelude::consume_context() }