diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index fa6686a46..38ce9c92f 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,7 +1,7 @@ -use crate::{nodes::RenderReturn, Component}; +use crate::{nodes::RenderReturn, ComponentFunction}; use std::{any::Any, panic::AssertUnwindSafe}; -pub type BoxedAnyProps = Box; +pub(crate) type BoxedAnyProps = Box; /// A trait that essentially allows VComponentProps to be used generically pub(crate) trait AnyProps { @@ -12,8 +12,8 @@ pub(crate) trait AnyProps { } /// Create a new boxed props object. -pub fn new_any_props( - render_fn: Component

, +pub(crate) fn new_any_props, P: Clone + 'static, M: 'static>( + render_fn: F, memo: fn(&P, &P) -> bool, props: P, name: &'static str, @@ -23,17 +23,21 @@ pub fn new_any_props( memo, props, name, + phantom: std::marker::PhantomData, }) } -struct VProps

{ - render_fn: Component

, +struct VProps, P, M> { + render_fn: F, memo: fn(&P, &P) -> bool, props: P, name: &'static str, + phantom: std::marker::PhantomData, } -impl AnyProps for VProps

{ +impl + Clone, P: Clone + 'static, M: 'static> AnyProps + for VProps +{ fn memoize(&self, other: &dyn Any) -> bool { match other.downcast_ref::

() { Some(other) => (self.memo)(&self.props, other), @@ -47,7 +51,7 @@ impl AnyProps for VProps

{ fn render(&self) -> RenderReturn { let res = std::panic::catch_unwind(AssertUnwindSafe(move || { - (self.render_fn)(self.props.clone()) + self.render_fn.rebuild(self.props.clone()) })); match res { @@ -67,6 +71,7 @@ impl AnyProps for VProps

{ memo: self.memo, props: self.props.clone(), name: self.name, + phantom: std::marker::PhantomData, }) } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0972e534d..7aae4200a 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -520,7 +520,7 @@ pub struct VComponent { impl VComponent { /// Create a new [`VComponent`] variant - pub fn new( + pub fn new( component: impl ComponentFunction, props: P, fn_name: &'static str, @@ -528,9 +528,7 @@ impl VComponent { where P: Properties + 'static, { - let component = Rc::new(component); let render_fn = component.id(); - let component = component.as_component(); let props = new_any_props(component,

::memoize, props, fn_name); VComponent { diff --git a/packages/core/src/platform.rs b/packages/core/src/platform.rs index ea6e5e06e..99bf3f0cf 100644 --- a/packages/core/src/platform.rs +++ b/packages/core/src/platform.rs @@ -1,6 +1,9 @@ use std::any::Any; -use crate::{properties::ComponentFunction, Component, VirtualDom}; +use crate::{ + properties::{ComponentFunction, RootProps}, + VComponent, VirtualDom, +}; /// A boxed object that can be injected into a component's context. pub struct BoxedContext(Box); @@ -40,32 +43,38 @@ impl ClonableAny for T { } /// The platform-independent part of the config needed to launch an application. -pub struct CrossPlatformConfig { +pub struct CrossPlatformConfig { /// The root component function. - pub component: Component, - /// The props for the root component. - pub props: Props, + component: VComponent, /// The contexts to provide to the root component. - pub root_contexts: Vec, + root_contexts: Vec, } -impl CrossPlatformConfig { +impl CrossPlatformConfig { /// Create a new cross-platform config. - pub fn new( + pub fn new( component: impl ComponentFunction, props: Props, root_contexts: Vec, ) -> Self { Self { - component: ComponentFunction::as_component(std::rc::Rc::new(component)), - props, + component: VComponent::new( + move |props: RootProps| component.rebuild(props.0), + RootProps(props), + "root", + ), root_contexts, } } + /// Push a new context into the root component's context. + pub fn push_context(&mut self, context: T) { + self.root_contexts.push(BoxedContext::new(context)); + } + /// Build a virtual dom from the config. pub fn build_vdom(self) -> VirtualDom { - let mut vdom = VirtualDom::new_with_props(self.component, self.props); + let mut vdom = VirtualDom::new_with_component(self.component); for context in self.root_contexts { vdom.insert_boxed_root_context(context); @@ -76,18 +85,18 @@ impl CrossPlatformConfig { } /// A builder to launch a specific platform. -pub trait PlatformBuilder { +pub trait PlatformBuilder { /// The platform-specific config needed to launch an application. type Config: Default; /// Launch the app. - fn launch(config: CrossPlatformConfig, platform_config: Self::Config); + fn launch(config: CrossPlatformConfig, platform_config: Self::Config); } -impl PlatformBuilder for () { +impl PlatformBuilder for () { type Config = (); - fn launch(_: CrossPlatformConfig, _: Self::Config) { + fn launch(_: CrossPlatformConfig, _: Self::Config) { panic!("No platform is currently enabled. Please enable a platform feature for the dioxus crate."); } } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 4cb66e10e..447bbd6ea 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, rc::Rc}; +use std::any::TypeId; use crate::innerlude::*; @@ -45,6 +45,31 @@ impl Properties for () { } } +/// Root properties never need to be memoized, so we can use a dummy implementation. +pub(crate) struct RootProps

(pub P); + +impl

Clone for RootProps

+where + P: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl

Properties for RootProps

+where + P: Clone + 'static, +{ + type Builder = P; + fn builder() -> Self::Builder { + todo!() + } + fn memoize(&self, _other: &Self) -> bool { + true + } +} + // We allow components to use the () generic parameter if they have no props. This impl enables the "build" method // that the macros use to anonymously complete prop construction. pub struct EmptyBuilder; @@ -62,35 +87,35 @@ where } /// Any component that implements the `ComponentFn` trait can be used as a component. -pub trait ComponentFunction: 'static { +pub trait ComponentFunction: Clone + 'static { /// Get the type id of the component. fn id(&self) -> TypeId { TypeId::of::() } /// Convert the component to a function that takes props and returns an element. - fn as_component(self: Rc) -> Component; + fn rebuild(&self, props: Props) -> Element; } /// Accept pre-formed component render functions as components impl ComponentFunction

for Component

{ - fn as_component(self: Rc) -> Component

{ - self.as_ref().clone() + fn rebuild(&self, props: P) -> Element { + (self)(props) } } /// Accept any callbacks that take props -impl Element + 'static, P> ComponentFunction

for F { - fn as_component(self: Rc) -> Component

{ - self +impl Element + Clone + 'static, P> ComponentFunction

for F { + fn rebuild(&self, props: P) -> Element { + self(props) } } /// Accept any callbacks that take no props pub struct EmptyMarker; -impl Element + 'static> ComponentFunction<(), EmptyMarker> for F { - fn as_component(self: Rc) -> Rc Element> { - Rc::new(move |_| self()) +impl Element + Clone + 'static> ComponentFunction<(), EmptyMarker> for F { + fn rebuild(&self, _: ()) -> Element { + self() } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 243375a36..630f44b14 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -3,7 +3,6 @@ //! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust. use crate::{ - any_props::new_any_props, arena::ElementId, innerlude::{ DirtyScope, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeState, VNodeMount, @@ -11,10 +10,10 @@ use crate::{ }, nodes::RenderReturn, nodes::{Template, TemplateId}, - properties::ComponentFunction, + properties::RootProps, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, - AttributeValue, BoxedContext, Element, Event, Mutations, Task, + AttributeValue, BoxedContext, ComponentFunction, Element, Event, Mutations, Task, VComponent, }; use futures_util::{pin_mut, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -230,13 +229,6 @@ impl VirtualDom { Self::new_with_props(app, ()) } - /// Create a new virtualdom and build it immediately - pub fn prebuilt(app: fn() -> Element) -> Self { - let mut dom = Self::new(app); - dom.rebuild_in_place(); - dom - } - /// Create a new VirtualDom with the given properties for the root component. /// /// # Description @@ -271,6 +263,53 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { + let vcomponent = VComponent::new( + move |props: RootProps

| root.rebuild(props.0), + RootProps(root_props), + "root", + ); + + Self::new_with_component(vcomponent) + } + + /// Create a new virtualdom and build it immediately + pub fn prebuilt(app: fn() -> Element) -> Self { + let mut dom = Self::new(app); + dom.rebuild_in_place(); + dom + } + + /// Create a new VirtualDom with the given properties for the root component. + /// + /// # Description + /// + /// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders. + /// + /// This is useful when a component tree can be driven by external state (IE SSR) but it would be too expensive + /// to toss out the entire tree. + /// + /// + /// # Example + /// ```rust, ignore + /// #[derive(PartialEq, Props)] + /// struct SomeProps { + /// name: &'static str + /// } + /// + /// fn Example(cx: SomeProps) -> Element { + /// rsx!{ div{ "hello {cx.name}" } }) + /// } + /// + /// let dom = VirtualDom::new(Example); + /// ``` + /// + /// Note: the VirtualDom is not progressed on creation. You must either "run_with_deadline" or use "rebuild" to progress it. + /// + /// ```rust, ignore + /// let mut dom = VirtualDom::new_from_root(VComponent::new(Example, SomeProps { name: "jane" }, "Example")); + /// let mutations = dom.rebuild(); + /// ``` + pub fn new_with_component(root: VComponent) -> Self { let (tx, rx) = futures_channel::mpsc::unbounded(); let mut dom = Self { @@ -285,10 +324,7 @@ impl VirtualDom { suspended_scopes: Default::default(), }; - let root = dom.new_scope( - new_any_props(Rc::new(root).as_component(), |_, _| true, root_props, "app"), - "app", - ); + let root = dom.new_scope(root.props, "app"); // Unlike react, we provide a default error boundary that just renders the error as a string root.context() diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index fa1e18fc4..98cfd93b6 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -18,7 +18,6 @@ use dioxus_html::{ use std::{ cell::{Cell, RefCell}, collections::HashMap, - marker::PhantomData, rc::Rc, sync::Arc, }; @@ -29,10 +28,10 @@ use tao::{ }; /// The single top-level object that manages all the running windows, assets, shortcuts, etc -pub(crate) struct App { +pub(crate) struct App { // move the props into a cell so we can pop it out later to create the first window // iOS panics if we create a window before the event loop is started, so we toss them into a cell - pub(crate) dioxus_config: Cell>>, + pub(crate) dioxus_config: Cell>, pub(crate) cfg: Cell>, // Stuff we need mutable access to @@ -59,10 +58,10 @@ pub struct SharedContext { pub(crate) target: EventLoopWindowTarget, } -impl App { +impl App { pub fn new( cfg: Config, - dioxus_config: CrossPlatformConfig, + dioxus_config: CrossPlatformConfig, ) -> (EventLoop, Self) { let event_loop = EventLoopBuilder::::with_user_event().build(); diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index 3019d167e..bf2be758a 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -10,10 +10,7 @@ use tao::event::{Event, StartCause, WindowEvent}; /// /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime /// and is equivalent to calling launch_with_props with the tokio feature disabled. -pub fn launch_with_props_blocking( - dioxus_cfg: CrossPlatformConfig, - desktop_config: Config, -) { +pub fn launch_with_props_blocking(dioxus_cfg: CrossPlatformConfig, desktop_config: Config) { let (event_loop, mut app) = App::new(desktop_config, dioxus_cfg); event_loop.run(move |window_event, _, control_flow| { @@ -53,10 +50,10 @@ pub fn launch_with_props_blocking( /// The desktop renderer platform pub struct DesktopPlatform; -impl PlatformBuilder for DesktopPlatform { +impl PlatformBuilder for DesktopPlatform { type Config = Config; - fn launch(config: dioxus_core::CrossPlatformConfig, platform_config: Self::Config) { + fn launch(config: dioxus_core::CrossPlatformConfig, platform_config: Self::Config) { #[cfg(feature = "tokio")] tokio::runtime::Builder::new_multi_thread() .enable_all() diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index f32c423bd..bd2200f5f 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -7,19 +7,17 @@ use dioxus_core::prelude::*; use dioxus_core::{BoxedContext, CrossPlatformConfig, PlatformBuilder}; /// A builder for a fullstack app. -pub struct LaunchBuilder = CurrentPlatform> -{ - cross_platform_config: CrossPlatformConfig, - platform_config: Option<>::Config>, +pub struct LaunchBuilder { + cross_platform_config: CrossPlatformConfig, + platform_config: Option<::Config>, } // Default platform builder -impl LaunchBuilder { +impl LaunchBuilder { /// Create a new builder for your application. This will create a launch configuration for the current platform based on the features enabled on the `dioxus` crate. - pub fn new(component: impl ComponentFunction) -> Self - where - Props: Default, - { + pub fn new( + component: impl ComponentFunction, + ) -> Self { Self { cross_platform_config: CrossPlatformConfig::new( component, @@ -29,28 +27,29 @@ impl LaunchBuilder { platform_config: None, } } + + /// Create a new builder for your application with some root props. This will create a launch configuration for the current platform based on the features enabled on the `dioxus` crate. + pub fn new_with_props( + component: impl ComponentFunction, + props: Props, + ) -> Self { + Self { + cross_platform_config: CrossPlatformConfig::new(component, props, Default::default()), + platform_config: None, + } + } } -impl> LaunchBuilder { - /// Pass some props to your application. - pub fn props(mut self, props: Props) -> Self { - self.cross_platform_config.props = props; - self - } - +impl LaunchBuilder { /// Inject state into the root component's context. pub fn context(mut self, state: impl Any + Clone + 'static) -> Self { self.cross_platform_config - .root_contexts - .push(BoxedContext::new(state)); + .push_context(BoxedContext::new(state)); self } /// Provide a platform-specific config to the builder. - pub fn cfg( - mut self, - config: impl Into>::Config>>, - ) -> Self { + pub fn cfg(mut self, config: impl Into::Config>>) -> Self { if let Some(config) = config.into() { self.platform_config = Some(config); } @@ -68,7 +67,7 @@ impl> LaunchBuilder LaunchBuilder { +impl LaunchBuilder { /// Launch your web application. pub fn launch_web(self) { dioxus_web::WebPlatform::launch( @@ -79,7 +78,7 @@ impl LaunchBuilder { } #[cfg(feature = "desktop")] -impl LaunchBuilder { +impl LaunchBuilder { /// Launch your desktop application. pub fn launch_desktop(self) { dioxus_desktop::DesktopPlatform::launch( @@ -97,7 +96,7 @@ type CurrentPlatform = dioxus_web::WebPlatform; type CurrentPlatform = (); /// Launch your application without any additional configuration. See [`LaunchBuilder`] for more options. -pub fn launch(component: impl ComponentFunction) +pub fn launch(component: impl ComponentFunction) where Props: Default + Clone + 'static, { @@ -106,7 +105,7 @@ where #[cfg(feature = "web")] /// Launch your web application without any additional configuration. See [`LaunchBuilder`] for more options. -pub fn launch_web(component: impl ComponentFunction) +pub fn launch_web(component: impl ComponentFunction) where Props: Default + Clone + 'static, { @@ -115,7 +114,7 @@ where #[cfg(feature = "desktop")] /// Launch your desktop application without any additional configuration. See [`LaunchBuilder`] for more options. -pub fn launch_desktop(component: impl ComponentFunction) +pub fn launch_desktop(component: impl ComponentFunction) where Props: Default + Clone + 'static, { diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index 65602edf5..2de99f07d 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -609,15 +609,13 @@ impl RouteEnum { quote! { impl dioxus_core::ComponentFunction<#props> for #name { - fn as_component(self: ::std::rc::Rc) -> Component<#props> { - ::std::rc::Rc::new(move |props| { - let initial_route = self.as_ref().clone(); - rsx! { - ::dioxus_router::prelude::Router::<#name> { - config: move || props.take().initial_route(initial_route) - } + fn rebuild(&self, props: #props) -> dioxus_core::Element { + let initial_route = self.clone(); + rsx! { + ::dioxus_router::prelude::Router::<#name> { + config: move || props.take().initial_route(initial_route) } - }) + } } } } diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index bdd9511b4..9df564eac 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -99,10 +99,7 @@ mod rehydrate; /// wasm_bindgen_futures::spawn_local(app_fut); /// } /// ``` -pub async fn run_with_props( - dioxus_config: CrossPlatformConfig, - web_config: Config, -) { +pub async fn run_with_props(dioxus_config: CrossPlatformConfig, web_config: Config) { tracing::info!("Starting up"); let mut dom = dioxus_config.build_vdom(); diff --git a/packages/web/src/platform.rs b/packages/web/src/platform.rs index e365f52b0..38d20a85a 100644 --- a/packages/web/src/platform.rs +++ b/packages/web/src/platform.rs @@ -5,10 +5,10 @@ use crate::Config; /// The web renderer platform pub struct WebPlatform; -impl PlatformBuilder for WebPlatform { +impl PlatformBuilder for WebPlatform { type Config = Config; - fn launch(config: CrossPlatformConfig, platform_config: Self::Config) { + fn launch(config: CrossPlatformConfig, platform_config: Self::Config) { wasm_bindgen_futures::spawn_local(async move { crate::run_with_props(config, platform_config).await; });