diff --git a/Cargo.toml b/Cargo.toml index 2d8e2b064..7b8d3e7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ members = [ "packages/core", "packages/html-namespace", "packages/web", - "packages/webview" + # "packages/webview" # "packages/cli", # "packages/atoms", # "packages/ssr", diff --git a/README.md b/README.md index 610f8fb26..7170912a3 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,17 @@ If you know React, then you already know Dioxus. ### **Things you'll love ❤️:** -- Ergonomic design -- Minimal boilerplate +- **Ergonomic** design +- **Minimal** boilerplate - Familiar design and semantics - Simple build, test, and deploy -- Compile-time correct templating -- Support for html! and rsx! templating +- **Compile-time correct** templating +- Support for **fine-grained reactivity** +- Support for **html!** and **rsx!** templating - SSR, WASM, desktop, and mobile support -- Powerful and simple integrated state management -- Rust! (enums, static types, modules, efficiency) +- Support for **asynchronous** batched rendering +- Powerful and simple **integrated state management** +- **Rust!** (enums, static types, modules, efficiency) ## Get Started with... @@ -96,27 +98,28 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an ### Phase 1: The Basics -| Feature | Dioxus | React | Notes for Dioxus | -| ---------------------- | ------ | ----- | ------------------------------------------------ | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Context | ✅ | ✅ | share state through the tree | -| Hook | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Null components | ✅ | ✅ | allow returning no components | -| No-div components | ✅ | ✅ | components that render components | -| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| Suspense | 🛠 | 🛠 | schedule future render from future/promise | -| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | -| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | -| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| Feature | Dioxus | React | Notes for Dioxus | +| ----------------------- | ------ | ----- | ------------------------------------------------ | +| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | +| Keyed Components | ✅ | ✅ | advanced diffing with keys | +| Web | ✅ | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | ✅ | renderer for desktop | +| Context | ✅ | ✅ | share state through the tree | +| Hook | ✅ | ✅ | memory cells in components | +| SSR | ✅ | ✅ | render directly to string | +| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | +| Component Children | ✅ | ✅ | cx.children() as a list of nodes | +| Null components | ✅ | ✅ | allow returning no components | +| No-div components | ✅ | ✅ | components that render components | +| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | +| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | +| Fine-grained reactivity | 🛠 | ❓ | Skip diffing for fine-grain updates | +| Suspense | 🛠 | 🛠 | schedule future render from future/promise | +| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | +| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | +| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | - [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. - [2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific. diff --git a/docs/main-concepts/12-signals.md b/docs/main-concepts/12-signals.md new file mode 100644 index 000000000..f1036e30e --- /dev/null +++ b/docs/main-concepts/12-signals.md @@ -0,0 +1,7 @@ +# Signals: Skipping the Diff + +In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. + +What does this look like? + +## How does it work? diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 4ae54486c..8dcea1814 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -22,9 +22,6 @@ thiserror = "1" # faster hashmaps fxhash = "0.2.1" -# even *faster* hashmaps for index-based types -nohash-hasher = "0.2.0" - # Used in diffing longest-increasing-subsequence = "0.1.0" @@ -39,6 +36,8 @@ smallvec = "1.6.1" # Backs scopes and unique keys slotmap = "1.0.3" +# backs the fiber system for suspended components +# todo: would like to use something smaller or just roll our own futures manually futures = "0.3.15" diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 47cec84f1..deafc6be7 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -42,6 +42,7 @@ pros: cons: - cost of querying individual nodes (about 7ns per node query for all sizes w/ nohasher) +- 2-3 ns query cost with slotmap - old IDs need to be manually freed when subtrees are destroyed - can be collected as garbage after every render - loss of ids between renders........................ @@ -54,4 +55,10 @@ cons: ## idea: leak raw nodes and then reclaim them on drop -## idea: bind +# Fiber/Concurrency + +Dioxus is designed to support partial rendering. Partial rendering means that not _every_ component will be rendered on every tick. If some components were diffed. + +Any given component will only be rendered on a single thread, so data inside of components does not need to be send/sync. + +To schedule a render outside of the main component, the `suspense` method is exposed. `Suspense` consumes a future (valid for `bump) lifetime diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index bf2e5e0a5..43d3b7335 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -377,7 +377,6 @@ pub struct Scope { pub event_channel: Rc, - // pub event_queue: EventQueue, pub caller: Weak, pub hookidx: Cell, @@ -422,7 +421,6 @@ impl Scope { // Therefore, their lifetimes are connected exclusively to the virtual dom pub fn new<'creator_node>( caller: Weak, - // caller: Weak>, arena_idx: ScopeIdx, parent: Option, height: u32, diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 78a2fb6ee..4a979c42b 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -24,7 +24,6 @@ once_cell = "1.7.2" atoms = { path="../atoms" } async-channel = "1.6.1" -nohash-hasher = "0.2.0" anyhow = "1.0.41" slotmap = "1.0.3" diff --git a/packages/web/examples/todomvc-nodeps.rs b/packages/web/examples/todomvc-nodeps.rs new file mode 100644 index 000000000..51a424f7c --- /dev/null +++ b/packages/web/examples/todomvc-nodeps.rs @@ -0,0 +1,125 @@ +use std::{collections::HashMap, rc::Rc}; + +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; + +static APP_STYLE: &'static str = include_str!("./todomvc/style.css"); + +fn main() { + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +#[derive(PartialEq)] +pub enum FilterState { + All, + Active, + Completed, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct TodoItem { + pub id: uuid::Uuid, + pub checked: bool, + pub contents: String, +} + +pub fn App(cx: Context<()>) -> VNode { + let (draft, set_draft) = use_state(&cx, || "".to_string()); + let (todos, set_todos) = use_state(&cx, || HashMap::>::new()); + let (filter, set_filter) = use_state(&cx, || FilterState::All); + + let filtered_todos = todos.iter().filter(move |(id, item)| match filter { + FilterState::All => true, + FilterState::Active => !item.checked, + FilterState::Completed => item.checked, + }); + let items_left = filtered_todos.clone().count(); + let item_text = match items_left { + 1 => "item", + _ => "items", + }; + + cx.render(rsx! { + div { id: "app" + div { + header { class: "header" + h1 {"todos"} + input { + class: "new-todo" + placeholder: "What needs to be done?" + value: "{draft}" + oninput: move |evt| set_draft(evt.value()) + } + } + + {filtered_todos.map(|(id, item)| { + rsx!(TodoEntry { + key: "{id}", + item: item.clone() + }) + })} + + // filter toggle (show only if the list isn't empty) + {(!todos.is_empty()).then(|| rsx!( + footer { + span { + strong {"{items_left}"} + span {"{item_text} left"} + } + ul { + class: "filters" + li { class: "All", a { href: "", onclick: move |_| set_filter(FilterState::All), "All" }} + li { class: "Active", a { href: "active", onclick: move |_| set_filter(FilterState::Active), "Active" }} + li { class: "Completed", a { href: "completed", onclick: move |_| set_filter(FilterState::Completed), "Completed" }} + } + } + ))} + } + // footer + footer { + class: "info" + p {"Double-click to edit a todo"} + p { + "Created by " + a { "jkelleyrtp", href: "http://github.com/jkelleyrtp/" } + } + p { + "Part of " + a { "TodoMVC", href: "http://todomvc.com" } + } + } + } + }) +} + +#[derive(PartialEq, Props)] +pub struct TodoEntryProps { + item: Rc, +} + +pub fn TodoEntry(cx: Context) -> VNode { + let (is_editing, set_is_editing) = use_state(&cx, || false); + let contents = ""; + let todo = TodoItem { + checked: false, + contents: "asd".to_string(), + id: uuid::Uuid::new_v4(), + }; + + cx.render(rsx! ( + li { + "{todo.id}" + input { + class: "toggle" + type: "checkbox" + "{todo.checked}" + } + {is_editing.then(|| rsx!{ + input { + value: "{contents}" + } + })} + } + )) +} diff --git a/packages/web/examples/todomvcsingle.rs b/packages/web/examples/todomvcsingle.rs index f9f1ddf08..1e8207f87 100644 --- a/packages/web/examples/todomvcsingle.rs +++ b/packages/web/examples/todomvcsingle.rs @@ -8,6 +8,7 @@ //! //! Here, we show to use Dioxus' Recoil state management solution to simplify app logic #![allow(non_snake_case)] +use dioxus_core as dioxus; use dioxus_web::dioxus::prelude::*; use std::collections::HashMap; @@ -141,39 +142,27 @@ pub fn FilterToggles(cx: Context<()>) -> VNode { let reducer = TodoManager(use_recoil_api(cx)); let items_left = use_read(cx, &TODOS_LEFT); - let toggles = [ - ("All", "", FilterState::All), - ("Active", "active", FilterState::Active), - ("Completed", "completed", FilterState::Completed), - ] - .iter() - .map(|(name, path, filter)| { - rsx!( - li { class: "{name}" - a { - href: "{path}", - onclick: move |_| reducer.set_filter(&filter), - "{name}", - } - } - ) - }); - let item_text = match items_left { 1 => "item", _ => "items", }; + let toggles = rsx! { + ul { + class: "filters" + li { class: "All", a { href: "", onclick: move |_| reducer.set_filter(&FilterState::All), "All" }} + li { class: "Active", a { href: "active", onclick: move |_| reducer.set_filter(&FilterState::Active), "Active" }} + li { class: "Completed", a { href: "completed", onclick: move |_| reducer.set_filter(&FilterState::Completed), "Completed" }} + } + }; + rsx! { in cx, footer { span { strong {"{items_left}"} span { "{item_text} left" } } - ul { - class: "filters" - {toggles} - } + {toggles} } } } diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index 6e7cec382..ec0700fa9 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -6,7 +6,6 @@ use dioxus_core::{ virtual_dom::RealDomNode, }; use fxhash::FxHashMap; -use nohash_hasher::IntMap; use slotmap::{DefaultKey, Key, KeyData}; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ @@ -20,25 +19,19 @@ pub struct WebsysDom { root: Element, event_receiver: async_channel::Receiver, + trigger: Arc, - // every callback gets a monotomically increasing callback ID - callback_id: usize, - // map of listener types to number of those listeners - listeners: FxHashMap)>, - - // Map of callback_id to component index and listener id - callback_map: FxHashMap, + // This is roughly a delegater + // TODO: check how infero delegates its events - some are more performant + listeners: FxHashMap<&'static str, (usize, Closure)>, // We need to make sure to add comments between text nodes // We ensure that the text siblings are patched by preventing the browser from merging // neighboring text nodes. Originally inspired by some of React's work from 2016. // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes // -> https://github.com/facebook/react/pull/5753 - // - // `ptns` = Percy text node separator - // TODO last_node_was_text: bool, } impl WebsysDom { @@ -48,7 +41,7 @@ impl WebsysDom { .document() .expect("must have access to the Document"); - let (sender, mut receiver) = async_channel::unbounded::(); + let (sender, receiver) = async_channel::unbounded::(); let sender_callback = Arc::new(move |ev| { let c = sender.clone(); @@ -57,19 +50,13 @@ impl WebsysDom { }); }); - let mut nodes = slotmap::SlotMap::new(); - // HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default()); - // let mut nodes = - // HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default()); + let mut nodes = slotmap::SlotMap::with_capacity(1000); let root_id = nodes.insert(root.clone().dyn_into::().unwrap()); Self { stack: Stack::with_capacity(10), nodes, - - callback_id: 0, listeners: FxHashMap::default(), - callback_map: FxHashMap::default(), document, event_receiver: receiver, trigger: sender_callback, @@ -208,7 +195,7 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { fn new_event_listener( &mut self, - event: &str, + event: &'static str, scope: dioxus_core::prelude::ScopeIdx, el_id: usize, real_id: RealDomNode, @@ -231,10 +218,10 @@ impl<'a> dioxus_core::diff::RealDom<'a> for WebsysDom { .dyn_ref::() .expect(&format!("not an element: {:?}", el)); - let (gi_id, gi_gen) = (&scope).into_raw_parts(); + let gi_id = scope.data().as_ffi(); el.set_attribute( &format!("dioxus-event-{}", event), - &format!("{}.{}.{}.{}", gi_id, gi_gen, el_id, real_id.0), + &format!("{}.{}.{}", gi_id, el_id, real_id.0), ) .unwrap(); @@ -565,13 +552,9 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { .get_attribute(&format!("dioxus-event-{}", typ)) .context("")?; - let mut fields = val.splitn(4, "."); + let mut fields = val.splitn(3, "."); let gi_id = fields - .next() - .and_then(|f| f.parse::().ok()) - .context("")?; - let gi_gen = fields .next() .and_then(|f| f.parse::().ok()) .context("")?; @@ -585,17 +568,19 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { .context("")?; // Call the trigger - log::debug!( - "decoded gi_id: {}, gi_gen: {}, li_idx: {}", - gi_id, - gi_gen, - el_id - ); + log::debug!("decoded gi_id: {}, li_idx: {}", gi_id, el_id); - let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); + let triggered_scope: ScopeIdx = KeyData::from_ffi(gi_id).into(); Ok(EventTrigger::new( virtual_event_from_websys_event(event), triggered_scope, real_id, )) } + +struct ListenerMap {} +impl ListenerMap { + fn get(&self, event: &'static str) -> bool { + false + } +}