From 9731640434cb8f7b5079d0ac1862262e19c2ed6b Mon Sep 17 00:00:00 2001 From: DianQK Date: Wed, 21 Jun 2023 20:07:49 +0800 Subject: [PATCH 01/23] Revert "fix: use typeid of atoms for their unique ID (#558)" This reverts commit bf9901a6cd98d9487174f00b9b344f6105677017. --- packages/fermi/src/atoms/atom.rs | 9 +++------ packages/fermi/src/atoms/atomfamily.rs | 9 +++------ packages/fermi/src/atoms/atomref.rs | 7 ++----- packages/fermi/src/root.rs | 24 +++--------------------- 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs index 81e04ce0b..2b99174b5 100644 --- a/packages/fermi/src/atoms/atom.rs +++ b/packages/fermi/src/atoms/atom.rs @@ -3,7 +3,7 @@ use crate::{AtomId, AtomRoot, Readable, Writable}; pub type Atom = fn(AtomBuilder) -> T; pub struct AtomBuilder; -impl Readable for Atom { +impl Readable for Atom { fn read(&self, _root: AtomRoot) -> Option { todo!() } @@ -11,14 +11,11 @@ impl Readable for Atom { (*self)(AtomBuilder) } fn unique_id(&self) -> AtomId { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *self as *const () } } -impl Writable for Atom { +impl Writable for Atom { fn write(&self, _root: AtomRoot, _value: V) { todo!() } diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs index 29cecd4b2..60fc22bcb 100644 --- a/packages/fermi/src/atoms/atomfamily.rs +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -4,7 +4,7 @@ use im_rc::HashMap as ImMap; pub struct AtomFamilyBuilder; pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; -impl Readable> for AtomFamily { +impl Readable> for AtomFamily { fn read(&self, _root: AtomRoot) -> Option> { todo!() } @@ -14,14 +14,11 @@ impl Readable> for AtomFamily { } fn unique_id(&self) -> AtomId { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *self as *const () } } -impl Writable> for AtomFamily { +impl Writable> for AtomFamily { fn write(&self, _root: AtomRoot, _value: ImMap) { todo!() } diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs index c103fa662..59e412031 100644 --- a/packages/fermi/src/atoms/atomref.rs +++ b/packages/fermi/src/atoms/atomref.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; pub struct AtomRefBuilder; pub type AtomRef = fn(AtomRefBuilder) -> T; -impl Readable> for AtomRef { +impl Readable> for AtomRef { fn read(&self, _root: AtomRoot) -> Option> { todo!() } @@ -14,10 +14,7 @@ impl Readable> for AtomRef { } fn unique_id(&self) -> AtomId { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *self as *const () } } diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index 99e97bfbb..f77930c75 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -1,21 +1,11 @@ -use std::{ - any::{Any, TypeId}, - cell::RefCell, - collections::HashMap, - rc::Rc, - sync::Arc, -}; +use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc}; use dioxus_core::ScopeId; use im_rc::HashSet; use crate::Readable; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct AtomId { - pub ptr: *const (), - pub type_id: TypeId, -} +pub type AtomId = *const (); pub struct AtomRoot { pub atoms: RefCell>, @@ -54,15 +44,7 @@ impl AtomRoot { // initialize the value if it's not already initialized if let Some(slot) = atoms.get_mut(&f.unique_id()) { slot.subscribers.insert(scope); - match slot.value.clone().downcast() { - Ok(res) => res, - Err(e) => panic!( - "Downcasting atom failed: {:?}. Has typeid of {:?} but needs typeid of {:?}", - f.unique_id(), - e.type_id(), - TypeId::of::() - ), - } + slot.value.clone().downcast().unwrap() } else { let value = Rc::new(f.init()); let mut subscribers = HashSet::new(); From 535435a4cf7ad8c3a1e6cc9bdb82eb4f705d544e Mon Sep 17 00:00:00 2001 From: DianQK Date: Wed, 21 Jun 2023 20:09:11 +0800 Subject: [PATCH 02/23] Fix the issue of duplicate unique ID for atoms using `newtype`. The MergeFunctionsPass in LLVM merges identical functions (https://rust.godbolt.org/z/3nnr9nMne), resulting in the same function addresses. --- .github/workflows/macos.yml | 4 ++- .github/workflows/windows.yml | 1 + .../en/__unused/advanced-guides/12-signals.md | 4 +-- docs/guide/src/en/async/use_coroutine.md | 8 ++--- .../src/en/best_practices/error_handling.md | 6 ++-- docs/guide/src/pt-br/async/use_coroutine.md | 8 ++--- .../pt-br/best_practices/error_handling.md | 6 ++-- examples/fermi.rs | 10 +++---- packages/fermi/README.md | 20 ++++++------- packages/fermi/src/atoms/atom.rs | 30 ++++++++++++++----- packages/fermi/src/atoms/atomfamily.rs | 10 +++---- packages/fermi/src/atoms/atomref.rs | 12 ++++---- packages/fermi/src/hooks/atom_ref.rs | 5 +++- packages/fermi/src/hooks/state.rs | 2 +- 14 files changed, 74 insertions(+), 52 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index b80a7b1f5..eef04d521 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -33,4 +33,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: actions/checkout@v3 - - run: cargo test --all --tests + - run: | + cargo test --all --tests + cargo test --package fermi --release diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 0dd8a1842..368df1630 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -85,4 +85,5 @@ jobs: set RUST_BACKTRACE=1 cargo build --all --tests --examples cargo test --all --tests + cargo test --package fermi --release shell: cmd diff --git a/docs/guide/src/en/__unused/advanced-guides/12-signals.md b/docs/guide/src/en/__unused/advanced-guides/12-signals.md index c7012a119..486758c8d 100644 --- a/docs/guide/src/en/__unused/advanced-guides/12-signals.md +++ b/docs/guide/src/en/__unused/advanced-guides/12-signals.md @@ -95,7 +95,7 @@ Calling `deref` or `deref_mut` is actually more complex than it seems. When a va Sometimes you want a signal to propagate across your app, either through far-away siblings or through deeply-nested components. In these cases, we use Dirac: Dioxus's first-class state management toolkit. Dirac atoms automatically implement the Signal API. This component will bind the input element to the `TITLE` atom. ```rust -const TITLE: Atom = || "".to_string(); +const TITLE: Atom = Atom(|| "".to_string()); const Provider: Component = |cx|{ let title = use_signal(cx, &TITLE); render!(input { value: title }) @@ -131,7 +131,7 @@ By default, Dioxus is limited when you use iter/map. With the `For` component, y Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. ```rust -const DICT: AtomFamily = |_| {}; +const DICT: AtomFamily = AtomFamily(|_| {}); const List: Component = |cx|{ let dict = use_signal(cx, &DICT); cx.render(rsx!( diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index 537e27ea4..caffb6bbc 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/docs/guide/src/en/async/use_coroutine.md @@ -146,7 +146,7 @@ async fn editor_service(rx: UnboundedReceiver) { We can combine coroutines with [Fermi](https://docs.rs/fermi/latest/fermi/index.html) to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux – the only Atoms that need to exist are those that are used to drive the display/UI. ```rust -static USERNAME: Atom = |_| "default".to_string(); +static USERNAME: Atom = Atom(|_| "default".to_string()); fn app(cx: Scope) -> Element { let atoms = use_atom_root(cx); @@ -159,7 +159,7 @@ fn app(cx: Scope) -> Element { } fn Banner(cx: Scope) -> Element { - let username = use_read(cx, USERNAME); + let username = use_read(cx, &USERNAME); cx.render(rsx!{ h1 { "Welcome back, {username}" } @@ -177,8 +177,8 @@ enum SyncAction { } async fn sync_service(mut rx: UnboundedReceiver, atoms: AtomRoot) { - let username = atoms.write(USERNAME); - let errors = atoms.write(ERRORS); + let username = atoms.write(&USERNAME); + let errors = atoms.write(&ERRORS); while let Ok(msg) = rx.next().await { match msg { diff --git a/docs/guide/src/en/best_practices/error_handling.md b/docs/guide/src/en/best_practices/error_handling.md index afbb45417..992926054 100644 --- a/docs/guide/src/en/best_practices/error_handling.md +++ b/docs/guide/src/en/best_practices/error_handling.md @@ -118,14 +118,14 @@ enum InputError { TooShort, } -static INPUT_ERROR: Atom = |_| InputError::None; +static INPUT_ERROR: Atom = Atom(|_| InputError::None); ``` Then, in our top level component, we want to explicitly handle the possible error state for this part of the tree. ```rust fn TopLevel(cx: Scope) -> Element { - let error = use_read(cx, INPUT_ERROR); + let error = use_read(cx, &INPUT_ERROR); match error { TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), @@ -139,7 +139,7 @@ Now, whenever a downstream component has an error in its actions, it can simply ```rust fn Commandline(cx: Scope) -> Element { - let set_error = use_set(cx, INPUT_ERROR); + let set_error = use_set(cx, &INPUT_ERROR); cx.render(rsx!{ input { diff --git a/docs/guide/src/pt-br/async/use_coroutine.md b/docs/guide/src/pt-br/async/use_coroutine.md index cd7cbff11..b6fd71c6c 100644 --- a/docs/guide/src/pt-br/async/use_coroutine.md +++ b/docs/guide/src/pt-br/async/use_coroutine.md @@ -105,7 +105,7 @@ async fn editor_service(rx: UnboundedReceiver) { Podemos combinar corrotinas com `Fermi` para emular o sistema `Thunk` do **Redux Toolkit** com muito menos dor de cabeça. Isso nos permite armazenar todo o estado do nosso aplicativo _dentro_ de uma tarefa e, em seguida, simplesmente atualizar os valores de "visualização" armazenados em `Atoms`. Não pode ser subestimado o quão poderosa é essa técnica: temos todas as vantagens das tarefas nativas do Rust com as otimizações e ergonomia do estado global. Isso significa que seu estado _real_ não precisa estar vinculado a um sistema como `Fermi` ou `Redux` – os únicos `Atoms` que precisam existir são aqueles que são usados para controlar a interface. ```rust -static USERNAME: Atom = |_| "default".to_string(); +static USERNAME: Atom = Atom(|_| "default".to_string()); fn app(cx: Scope) -> Element { let atoms = use_atom_root(cx); @@ -118,7 +118,7 @@ fn app(cx: Scope) -> Element { } fn Banner(cx: Scope) -> Element { - let username = use_read(cx, USERNAME); + let username = use_read(cx, &USERNAME); cx.render(rsx!{ h1 { "Welcome back, {username}" } @@ -134,8 +134,8 @@ enum SyncAction { } async fn sync_service(mut rx: UnboundedReceiver, atoms: AtomRoot) { - let username = atoms.write(USERNAME); - let errors = atoms.write(ERRORS); + let username = atoms.write(&USERNAME); + let errors = atoms.write(&ERRORS); while let Ok(msg) = rx.next().await { match msg { diff --git a/docs/guide/src/pt-br/best_practices/error_handling.md b/docs/guide/src/pt-br/best_practices/error_handling.md index 938322234..e0a7be509 100644 --- a/docs/guide/src/pt-br/best_practices/error_handling.md +++ b/docs/guide/src/pt-br/best_practices/error_handling.md @@ -113,14 +113,14 @@ enum InputError { TooShort, } -static INPUT_ERROR: Atom = |_| InputError::None; +static INPUT_ERROR: Atom = Atom(|_| InputError::None); ``` Então, em nosso componente de nível superior, queremos tratar explicitamente o possível estado de erro para esta parte da árvore. ```rust fn TopLevel(cx: Scope) -> Element { - let error = use_read(cx, INPUT_ERROR); + let error = use_read(cx, &INPUT_ERROR); match error { TooLong => return cx.render(rsx!{ "FAILED: Too long!" }), @@ -134,7 +134,7 @@ Agora, sempre que um componente _downstream_ tiver um erro em suas ações, ele ```rust fn Commandline(cx: Scope) -> Element { - let set_error = use_set(cx, INPUT_ERROR); + let set_error = use_set(cx, &INPUT_ERROR); cx.render(rsx!{ input { diff --git a/examples/fermi.rs b/examples/fermi.rs index 001890892..a15fc807f 100644 --- a/examples/fermi.rs +++ b/examples/fermi.rs @@ -7,11 +7,11 @@ fn main() { dioxus_desktop::launch(app) } -static NAME: Atom = |_| "world".to_string(); +static NAME: Atom = Atom(|_| "world".to_string()); fn app(cx: Scope) -> Element { use_init_atom_root(cx); - let name = use_read(cx, NAME); + let name = use_read(cx, &NAME); cx.render(rsx! { div { "hello {name}!" } @@ -21,7 +21,7 @@ fn app(cx: Scope) -> Element { } fn Child(cx: Scope) -> Element { - let set_name = use_set(cx, NAME); + let set_name = use_set(cx, &NAME); cx.render(rsx! { button { @@ -31,10 +31,10 @@ fn Child(cx: Scope) -> Element { }) } -static NAMES: AtomRef> = |_| vec!["world".to_string()]; +static NAMES: AtomRef> = AtomRef(|_| vec!["world".to_string()]); fn ChildWithRef(cx: Scope) -> Element { - let names = use_atom_ref(cx, NAMES); + let names = use_atom_ref(cx, &NAMES); cx.render(rsx! { div { diff --git a/packages/fermi/README.md b/packages/fermi/README.md index f04ec5961..f8192bbc7 100644 --- a/packages/fermi/README.md +++ b/packages/fermi/README.md @@ -1,4 +1,3 @@ -

Fermi ⚛

@@ -6,7 +5,6 @@

- ------ +--- Fermi is a global state management solution for Dioxus that's as easy as `use_state`. Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`: ```rust, ignore -static NAME: Atom<&str> = |_| "Dioxus"; +static NAME: Atom<&str> = Atom(|_| "Dioxus"); ``` From anywhere in our app, we can read the value of our atom: -```rust, ignores +```rust, ignore fn NameCard(cx: Scope) -> Element { - let name = use_read(cx, NAME); + let name = use_read(cx, &NAME); cx.render(rsx!{ h1 { "Hello, {name}"} }) } ``` @@ -53,7 +51,7 @@ We can also set the value of our atom, also from anywhere in our app: ```rust, ignore fn NameCard(cx: Scope) -> Element { - let set_name = use_set(cx, NAME); + let set_name = use_set(cx, &NAME); cx.render(rsx!{ button { onclick: move |_| set_name("Fermi"), @@ -66,10 +64,10 @@ fn NameCard(cx: Scope) -> Element { If needed, we can update the atom's value, based on itself: ```rust, ignore -static COUNT: Atom = |_| 0; +static COUNT: Atom = Atom(|_| 0); fn Counter(cx: Scope) -> Element { - let mut count = use_atom_state(cx, COUNT); + let mut count = use_atom_state(cx, &COUNT); cx.render(rsx!{ p { @@ -86,6 +84,7 @@ fn Counter(cx: Scope) -> Element { It's that simple! ## Installation + Fermi is currently under construction, so you have to use the `master` branch to get started. ```toml @@ -93,10 +92,10 @@ Fermi is currently under construction, so you have to use the `master` branch to fermi = { git = "https://github.com/dioxuslabs/dioxus" } ``` - ## Running examples The examples here use Dioxus Desktop to showcase their functionality. To run an example, use + ```sh $ cargo run --example fermi ``` @@ -104,6 +103,7 @@ $ cargo run --example fermi ## Features Broadly our feature set required to be released includes: + - [x] Support for Atoms - [x] Support for AtomRef (for values that aren't `Clone`) - [ ] Support for Atom Families diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs index 2b99174b5..bba1f532f 100644 --- a/packages/fermi/src/atoms/atom.rs +++ b/packages/fermi/src/atoms/atom.rs @@ -1,21 +1,21 @@ use crate::{AtomId, AtomRoot, Readable, Writable}; -pub type Atom = fn(AtomBuilder) -> T; +pub struct Atom(pub fn(AtomBuilder) -> T); pub struct AtomBuilder; -impl Readable for Atom { +impl Readable for &'static Atom { fn read(&self, _root: AtomRoot) -> Option { todo!() } fn init(&self) -> V { - (*self)(AtomBuilder) + self.0(AtomBuilder) } fn unique_id(&self) -> AtomId { - *self as *const () + *self as *const Atom as *const () } } -impl Writable for Atom { +impl Writable for &'static Atom { fn write(&self, _root: AtomRoot, _value: V) { todo!() } @@ -23,6 +23,22 @@ impl Writable for Atom { #[test] fn atom_compiles() { - static TEST_ATOM: Atom<&str> = |_| "hello"; - dbg!(TEST_ATOM.init()); + static TEST_ATOM: Atom<&str> = Atom(|_| "hello"); + dbg!((&TEST_ATOM).init()); +} + +#[test] +fn atom_is_unique() { + static TEST_ATOM_1: Atom<&str> = Atom(|_| "hello"); + static TEST_ATOM_2: Atom<&str> = Atom(|_| "hello"); + assert_eq!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_1).unique_id()); + assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id()); +} + +#[test] +fn atom_is_unique_2() { + struct S(String); + static TEST_ATOM_1: Atom> = Atom(|_| Vec::new()); + static TEST_ATOM_2: Atom> = Atom(|_| Vec::new()); + assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id()); } diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs index 60fc22bcb..45f6b8feb 100644 --- a/packages/fermi/src/atoms/atomfamily.rs +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -2,23 +2,23 @@ use crate::{AtomId, AtomRoot, Readable, Writable}; use im_rc::HashMap as ImMap; pub struct AtomFamilyBuilder; -pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; +pub struct AtomFamily(pub fn(AtomFamilyBuilder) -> ImMap); -impl Readable> for AtomFamily { +impl Readable> for &'static AtomFamily { fn read(&self, _root: AtomRoot) -> Option> { todo!() } fn init(&self) -> ImMap { - (*self)(AtomFamilyBuilder) + self.0(AtomFamilyBuilder) } fn unique_id(&self) -> AtomId { - *self as *const () + *self as *const AtomFamily as *const () } } -impl Writable> for AtomFamily { +impl Writable> for &'static AtomFamily { fn write(&self, _root: AtomRoot, _value: ImMap) { todo!() } diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs index 59e412031..5e863ade2 100644 --- a/packages/fermi/src/atoms/atomref.rs +++ b/packages/fermi/src/atoms/atomref.rs @@ -2,24 +2,24 @@ use crate::{AtomId, AtomRoot, Readable}; use std::cell::RefCell; pub struct AtomRefBuilder; -pub type AtomRef = fn(AtomRefBuilder) -> T; +pub struct AtomRef(pub fn(AtomRefBuilder) -> T); -impl Readable> for AtomRef { +impl Readable> for &'static AtomRef { fn read(&self, _root: AtomRoot) -> Option> { todo!() } fn init(&self) -> RefCell { - RefCell::new((*self)(AtomRefBuilder)) + RefCell::new(self.0(AtomRefBuilder)) } fn unique_id(&self) -> AtomId { - *self as *const () + *self as *const AtomRef as *const () } } #[test] fn atom_compiles() { - static TEST_ATOM: AtomRef> = |_| vec![]; - dbg!(TEST_ATOM.init()); + static TEST_ATOM: AtomRef> = AtomRef(|_| vec![]); + dbg!((&TEST_ATOM).init()); } diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs index 95732c491..74dd9268b 100644 --- a/packages/fermi/src/hooks/atom_ref.rs +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -13,7 +13,10 @@ use std::{ /// /// /// -pub fn use_atom_ref(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { +pub fn use_atom_ref<'a, T: 'static>( + cx: &'a ScopeState, + atom: &'static AtomRef, +) -> &'a UseAtomRef { let root = use_atom_root(cx); &cx.use_hook(|| { diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs index 15310cff2..071b18059 100644 --- a/packages/fermi/src/hooks/state.rs +++ b/packages/fermi/src/hooks/state.rs @@ -19,7 +19,7 @@ use std::{ /// static COUNT: Atom = |_| 0; /// /// fn Example(cx: Scope) -> Element { -/// let mut count = use_atom_state(cx, COUNT); +/// let mut count = use_atom_state(cx, &COUNT); /// /// cx.render(rsx! { /// div { From 8a2f9f3fcb37e63b64a92a6ce01511b5137dd122 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Wed, 28 Jun 2023 00:15:00 +0200 Subject: [PATCH 03/23] feat(docs): Improved shared state, use_effect and use_memo docs --- docs/guide/src/en/SUMMARY.md | 2 + docs/guide/src/en/async/use_effect.md | 32 +++++++++++ .../guide/src/en/interactivity/memoization.md | 23 ++++++++ .../src/en/interactivity/sharing_state.md | 13 +++-- examples/shared_state.rs | 54 +++++++++++++++++++ packages/hooks/src/useeffect.rs | 25 ++++++--- packages/hooks/src/usememo.rs | 28 ++++++---- 7 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 docs/guide/src/en/async/use_effect.md create mode 100644 docs/guide/src/en/interactivity/memoization.md create mode 100644 examples/shared_state.rs diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 62ef5adf7..a44439200 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -21,10 +21,12 @@ - [Hooks & Component State](interactivity/hooks.md) - [User Input](interactivity/user_input.md) - [Sharing State](interactivity/sharing_state.md) + - [Memoization](interactivity/memoization.md) - [Custom Hooks](interactivity/custom_hooks.md) - [Dynamic Rendering](interactivity/dynamic_rendering.md) - [Routing](interactivity/router.md) - [Async](async/index.md) + - [UseEffect](async/use_effect.md) - [UseFuture](async/use_future.md) - [UseCoroutine](async/use_coroutine.md) - [Spawning Futures](async/spawn.md) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md new file mode 100644 index 000000000..711492500 --- /dev/null +++ b/docs/guide/src/en/async/use_effect.md @@ -0,0 +1,32 @@ +# UseEffect + +[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) provides a future that executes after the hooks have been applied. + +Whenever the hooks dependencies change, the future will be re-evaluated. This is useful to syncrhonize with external events. + +If a future is pending when the dependencies change, the previous future will be allowed to continue + +> The `dependencies` is tuple of references to values that are `PartialEq + Clone`. + +```rust, no_run +#[inline_props] +fn Profile(cx: Scope, id: &str) -> Element { + let name = use_state(cx, || "Default name"); + + // Only fetch the user data when the id changes. + use_effect(cx, (id,), |(id,)| async move { + let user = fetch_user(id).await; + name.set(user.name); + }); + + render!( + p { "{name}" } + ) +} + +fn app(cx: Scope) -> Element { + render!( + Profile { id: "dioxusLabs" } + ) +} +``` diff --git a/docs/guide/src/en/interactivity/memoization.md b/docs/guide/src/en/interactivity/memoization.md new file mode 100644 index 000000000..cc67c02c4 --- /dev/null +++ b/docs/guide/src/en/interactivity/memoization.md @@ -0,0 +1,23 @@ +# Memoization + +[`use_memo`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_memo.html) let's you memorize values and therefore save computation time. This is useful for expensive calculations. + +```rust, no_run +#[inline_props] +fn Calculator(cx: Scope, number: usize) -> Element { + let bigger_number = use_memo(cx, (number,), |(number,)| { + // This will only be calculated when `number` has changed. + number * 100 + }): + + render!( + p { "{bigger_number}" } + ) +} + +fn app(cx: Scope) -> Element { + render!( + Calculator { number: 0 } + ) +} +``` diff --git a/docs/guide/src/en/interactivity/sharing_state.md b/docs/guide/src/en/interactivity/sharing_state.md index 75ace8973..3f9b3cdc2 100644 --- a/docs/guide/src/en/interactivity/sharing_state.md +++ b/docs/guide/src/en/interactivity/sharing_state.md @@ -11,6 +11,7 @@ Suppose we want to build a meme editor. We want to have an input to edit the mem > Of course, in this simple example, we could write everything in one component – but it is better to split everything out in smaller components to make the code more reusable, maintainable, and performant (this is even more important for larger, complex apps). We start with a `Meme` component, responsible for rendering a meme with a given caption: + ```rust {{#include ../../../examples/meme_editor.rs:meme_component}} ``` @@ -24,12 +25,14 @@ We also create a caption editor, completely decoupled from the meme. The caption ``` Finally, a third component will render the other two as children. It will be responsible for keeping the state and passing down the relevant props. + ```rust {{#include ../../../examples/meme_editor.rs:meme_editor}} ``` + ![Meme Editor Screenshot: An old plastic skeleton sitting on a park bench. Caption: "me waiting for a language feature"](./images/meme_editor_screenshot.png) -## Using Context +## Using Shared State Sometimes, some state needs to be shared between multiple components far down the tree, and passing it down through props is very inconvenient. @@ -39,7 +42,7 @@ Suppose now that we want to implement a dark mode toggle for our app. To achieve Now, we could write another `use_state` in the top component, and pass `is_dark_mode` down to every component through props. But think about what will happen as the app grows in complexity – almost every component that renders any CSS is going to need to know if dark mode is enabled or not – so they'll all need the same dark mode prop. And every parent component will need to pass it down to them. Imagine how messy and verbose that would get, especially if we had components several levels deep! -Dioxus offers a better solution than this "prop drilling" – providing context. The [`use_context_provider`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context_provider.html) hook is similar to `use_ref`, but it makes it available through [`use_context`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_context.html) for all children components. +Dioxus offers a better solution than this "prop drilling" – providing context. The [`use_shared_state_provider`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_shared_state_provider.html) hook is similar to `use_ref`, but it makes it available through [`use_shared_state`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_shared_state.html) for all children components. First, we have to create a struct for our dark mode configuration: @@ -48,19 +51,21 @@ First, we have to create a struct for our dark mode configuration: ``` Now, in a top-level component (like `App`), we can provide the `DarkMode` context to all children components: + ```rust {{#include ../../../examples/meme_editor_dark_mode.rs:context_provider}} ``` As a result, any child component of `App` (direct or not), can access the `DarkMode` context. + ```rust {{#include ../../../examples/meme_editor_dark_mode.rs:use_context}} ``` -> `use_context` returns `Option>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`. +> `use_shared_state` returns `Option>` here. If the context has been provided, the value is `Some(UseSharedState)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`. For example, here's how we would implement the dark mode toggle, which both reads the context (to determine what color it should render) and writes to it (to toggle dark mode): + ```rust {{#include ../../../examples/meme_editor_dark_mode.rs:toggle}} ``` - diff --git a/examples/shared_state.rs b/examples/shared_state.rs new file mode 100644 index 000000000..1072ce50d --- /dev/null +++ b/examples/shared_state.rs @@ -0,0 +1,54 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(App); +} + +struct DarkMode(bool); + +#[rustfmt::skip] +pub fn App(cx: Scope) -> Element { + use_shared_state_provider(cx, || DarkMode(false)); + + render!( + DarkModeToggle {}, + AppBody {} + ) +} + +pub fn DarkModeToggle(cx: Scope) -> Element { + let dark_mode = use_shared_state::(cx).unwrap(); + + let style = if dark_mode.read().0 { + "color:white" + } else { + "" + }; + + cx.render(rsx!(label { + style: "{style}", + "Dark Mode", + input { + r#type: "checkbox", + oninput: move |event| { + let is_enabled = event.value == "true"; + dark_mode.write().0 = is_enabled; + }, + }, + })) +} + +fn AppBody(cx: Scope) -> Element { + let dark_mode = use_shared_state::(cx).unwrap(); + + let is_dark_mode = dark_mode.read().0; + let answer = if is_dark_mode { "Yes" } else { "No" }; + + render!( + p { + "Is Dark mode enabled? {answer}" + } + ) +} diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index f9ef70b84..b52b89c3c 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -9,17 +9,30 @@ use crate::UseFutureDep; /// If a future is pending when the dependencies change, the previous future /// will be allowed to continue /// -/// - dependencies: a tuple of references to values that are PartialEq + Clone +/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone` /// /// ## Examples /// -/// ```rust, ignore +/// ```rust, no_run /// /// #[inline_props] -/// fn app(cx: Scope, name: &str) -> Element { -/// use_effect(cx, (name,), |(name,)| async move { -/// set_title(name); -/// })) +/// fn Profile(cx: Scope, id: &str) -> Element { +/// let name = use_state(cx, || "Default name"); +/// +/// use_effect(cx, (id,), |(id,)| async move { +/// let user = fetch_user(id).await; +/// name.set(user.name); +/// }); +/// +/// render!( +/// p { "{name}" } +/// ) +/// } +/// +/// fn app(cx: Scope) -> Element { +/// render!( +/// Profile { id: "dioxusLabs" } +/// ) /// } /// ``` pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) diff --git a/packages/hooks/src/usememo.rs b/packages/hooks/src/usememo.rs index 8cc5540e6..f99d1e893 100644 --- a/packages/hooks/src/usememo.rs +++ b/packages/hooks/src/usememo.rs @@ -2,21 +2,31 @@ use dioxus_core::ScopeState; use crate::UseFutureDep; -/// A hook that provides a callback that executes after the hooks have been applied +/// A hook that provides a callback that executes if the dependencies change. +/// This is useful to avoid running computation-expensive calculations even when the data doesn't change. /// -/// Whenever the hooks dependencies change, the callback will be re-evaluated. -/// -/// - dependencies: a tuple of references to values that are PartialEq + Clone +/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone` /// /// ## Examples /// -/// ```rust, ignore +/// ```rust, no_run /// /// #[inline_props] -/// fn app(cx: Scope, name: &str) -> Element { -/// use_memo(cx, (name,), |(name,)| { -/// expensive_computation(name); -/// })) +/// fn Calculator(cx: Scope, number: usize) -> Element { +/// let bigger_number = use_memo(cx, (number,), |(number,)| { +/// // This will only be calculated when `number` has changed. +/// number * 100 +/// }): +/// +/// render!( +/// p { "{bigger_number}" } +/// ) +/// } +/// +/// fn app(cx: Scope) -> Element { +/// render!( +/// Calculator { number: 0 } +/// ) /// } /// ``` pub fn use_memo(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T From a3978d60a898088c68376c51b39c30452c78b854 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Wed, 28 Jun 2023 00:17:36 +0200 Subject: [PATCH 04/23] update --- examples/shared_state.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 1072ce50d..6cd4d874e 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -21,23 +21,13 @@ pub fn App(cx: Scope) -> Element { pub fn DarkModeToggle(cx: Scope) -> Element { let dark_mode = use_shared_state::(cx).unwrap(); - let style = if dark_mode.read().0 { - "color:white" - } else { - "" - }; - - cx.render(rsx!(label { - style: "{style}", - "Dark Mode", - input { - r#type: "checkbox", - oninput: move |event| { - let is_enabled = event.value == "true"; - dark_mode.write().0 = is_enabled; - }, + render!(input { + r#type: "checkbox", + oninput: move |event| { + let is_enabled = event.value == "true"; + dark_mode.write().0 = is_enabled; }, - })) + }) } fn AppBody(cx: Scope) -> Element { From bf0247a7dc84aeafd2f91656f1e7bcc5b19298de Mon Sep 17 00:00:00 2001 From: marc2332 Date: Wed, 28 Jun 2023 20:01:28 +0200 Subject: [PATCH 05/23] tweaks --- docs/guide/src/en/async/use_effect.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index 711492500..8b82828db 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -2,11 +2,14 @@ [`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) provides a future that executes after the hooks have been applied. -Whenever the hooks dependencies change, the future will be re-evaluated. This is useful to syncrhonize with external events. +Whenever the hooks [dependencies](#dependencies) change, the future will be re-evaluated. This is useful to syncrhonize with external events. -If a future is pending when the dependencies change, the previous future will be allowed to continue -> The `dependencies` is tuple of references to values that are `PartialEq + Clone`. +## Dependencies + +You might want to call `use_effect` only when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. + +Example: ```rust, no_run #[inline_props] @@ -29,4 +32,4 @@ fn app(cx: Scope) -> Element { Profile { id: "dioxusLabs" } ) } -``` +``` \ No newline at end of file From d00b10e83ec9b75eb776b6093686e661d50d23b3 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 29 Jun 2023 14:14:29 +0200 Subject: [PATCH 06/23] tweaks --- docs/guide/src/en/async/use_effect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index 8b82828db..4980e2d20 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -7,7 +7,7 @@ Whenever the hooks [dependencies](#dependencies) change, the future will be re-e ## Dependencies -You might want to call `use_effect` only when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. +You can make the future re-run when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. Example: From fb2669c3be5800ff9ab9a8ca680283021bdda445 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 2 Jul 2023 22:41:00 +0200 Subject: [PATCH 07/23] improved shared state example --- examples/shared_state.rs | 89 +++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 6cd4d874e..2d5f6f125 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -1,44 +1,77 @@ #![allow(non_snake_case)] +use std::collections::HashMap; + use dioxus::prelude::*; fn main() { dioxus_desktop::launch(App); } -struct DarkMode(bool); +#[derive(Default)] +struct CoolData { + data: HashMap, +} + +impl CoolData { + pub fn new(data: HashMap) -> Self { + Self { data } + } + + pub fn view(&self, id: &usize) -> Option<&String> { + self.data.get(id) + } + + pub fn set(&mut self, id: usize, data: String) { + self.data.insert(id, data); + } +} #[rustfmt::skip] pub fn App(cx: Scope) -> Element { - use_shared_state_provider(cx, || DarkMode(false)); + use_shared_state_provider(cx, || CoolData::new(HashMap::from([ + (0, "Hello, World!".to_string()), + (1, "Dioxus is amazing!".to_string()) + ]))); render!( - DarkModeToggle {}, - AppBody {} - ) -} - -pub fn DarkModeToggle(cx: Scope) -> Element { - let dark_mode = use_shared_state::(cx).unwrap(); - - render!(input { - r#type: "checkbox", - oninput: move |event| { - let is_enabled = event.value == "true"; - dark_mode.write().0 = is_enabled; - }, - }) -} - -fn AppBody(cx: Scope) -> Element { - let dark_mode = use_shared_state::(cx).unwrap(); - - let is_dark_mode = dark_mode.read().0; - let answer = if is_dark_mode { "Yes" } else { "No" }; - - render!( - p { - "Is Dark mode enabled? {answer}" + DataEditor { + id: 0 + } + DataEditor { + id: 1 + } + DataView { + id: 0 + } + DataView { + id: 1 } ) } + +#[inline_props] +fn DataEditor(cx: Scope, id: usize) -> Element { + let cool_data = use_shared_state::(cx).unwrap().read(); + + let my_data = &cool_data.view(&id).unwrap(); + + render!(p { + "{my_data}" + }) +} + +#[inline_props] +fn DataView(cx: Scope, id: usize) -> Element { + let cool_data = use_shared_state::(cx).unwrap(); + + let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone()); + + let cool_data = cool_data.read(); + let my_data = &cool_data.view(&id).unwrap(); + + render!(input { + oninput: oninput, + value: "{my_data}" + }) +} From 052ae145bfb9fda38f72b43341d6059255282a61 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Sun, 2 Jul 2023 23:00:26 +0200 Subject: [PATCH 08/23] updated docs --- docs/guide/src/en/async/use_effect.md | 22 ++++++++++--------- .../guide/src/en/interactivity/memoization.md | 8 ++----- packages/hooks/src/useeffect.rs | 21 ++++++++++-------- packages/hooks/src/usememo.rs | 8 ++----- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index 4980e2d20..bd73453a9 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -4,7 +4,6 @@ Whenever the hooks [dependencies](#dependencies) change, the future will be re-evaluated. This is useful to syncrhonize with external events. - ## Dependencies You can make the future re-run when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. @@ -13,23 +12,26 @@ Example: ```rust, no_run #[inline_props] -fn Profile(cx: Scope, id: &str) -> Element { - let name = use_state(cx, || "Default name"); +fn Profile(cx: Scope, id: usize) -> Element { + let name = use_state(cx, || None); // Only fetch the user data when the id changes. - use_effect(cx, (id,), |(id,)| async move { - let user = fetch_user(id).await; - name.set(user.name); + use_effect(cx, (id,), |(id,)| { + to_owned![name]; + async move { + let user = fetch_user(id).await; + name.set(user.name); + } }); + let name = name.get().clone().unwrap_or("Loading...".to_string()); + render!( p { "{name}" } ) } fn app(cx: Scope) -> Element { - render!( - Profile { id: "dioxusLabs" } - ) + render!(Profile { id: 0 }) } -``` \ No newline at end of file +``` diff --git a/docs/guide/src/en/interactivity/memoization.md b/docs/guide/src/en/interactivity/memoization.md index cc67c02c4..72a709bcc 100644 --- a/docs/guide/src/en/interactivity/memoization.md +++ b/docs/guide/src/en/interactivity/memoization.md @@ -8,16 +8,12 @@ fn Calculator(cx: Scope, number: usize) -> Element { let bigger_number = use_memo(cx, (number,), |(number,)| { // This will only be calculated when `number` has changed. number * 100 - }): - + }); render!( p { "{bigger_number}" } ) } - fn app(cx: Scope) -> Element { - render!( - Calculator { number: 0 } - ) + render!(Calculator { number: 0 }) } ``` diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index b52b89c3c..aec1f9c9a 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -14,25 +14,28 @@ use crate::UseFutureDep; /// ## Examples /// /// ```rust, no_run -/// /// #[inline_props] -/// fn Profile(cx: Scope, id: &str) -> Element { -/// let name = use_state(cx, || "Default name"); +/// fn Profile(cx: Scope, id: usize) -> Element { +/// let name = use_state(cx, || None); /// -/// use_effect(cx, (id,), |(id,)| async move { -/// let user = fetch_user(id).await; -/// name.set(user.name); +/// // Only fetch the user data when the id changes. +/// use_effect(cx, (id,), |(id,)| { +/// to_owned![name]; +/// async move { +/// let user = fetch_user(id).await; +/// name.set(user.name); +/// } /// }); /// +/// let name = name.get().clone().unwrap_or("Loading...".to_string()); +/// /// render!( /// p { "{name}" } /// ) /// } /// /// fn app(cx: Scope) -> Element { -/// render!( -/// Profile { id: "dioxusLabs" } -/// ) +/// render!(Profile { id: 0 }) /// } /// ``` pub fn use_effect(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F) diff --git a/packages/hooks/src/usememo.rs b/packages/hooks/src/usememo.rs index f99d1e893..d3d991ccc 100644 --- a/packages/hooks/src/usememo.rs +++ b/packages/hooks/src/usememo.rs @@ -16,17 +16,13 @@ use crate::UseFutureDep; /// let bigger_number = use_memo(cx, (number,), |(number,)| { /// // This will only be calculated when `number` has changed. /// number * 100 -/// }): -/// +/// }); /// render!( /// p { "{bigger_number}" } /// ) /// } -/// /// fn app(cx: Scope) -> Element { -/// render!( -/// Calculator { number: 0 } -/// ) +/// render!(Calculator { number: 0 }) /// } /// ``` pub fn use_memo(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T From 1f3f72edc30e98fc0e61d1f940e49828beb1dc76 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 6 Jul 2023 17:23:37 +0200 Subject: [PATCH 09/23] updated --- docs/guide/src/en/async/use_effect.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index bd73453a9..f509921fc 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -1,14 +1,12 @@ # UseEffect -[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) provides a future that executes after the hooks have been applied. - -Whenever the hooks [dependencies](#dependencies) change, the future will be re-evaluated. This is useful to syncrhonize with external events. +[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) let's you run a callback that returns a future, only it's [dependencies](#dependencies) change. This is useful to syncrhonize with external events. ## Dependencies -You can make the future re-run when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run the future when any of those dependencies change. +You can make the callback re-run when some value changes. For example, you might want to fetch a user's data only when the user id changes. You can provide a tuple of "dependencies" to the hook. It will automatically re-run it when any of those dependencies change. -Example: +## Example ```rust, no_run #[inline_props] @@ -24,6 +22,12 @@ fn Profile(cx: Scope, id: usize) -> Element { } }); + // Because the dependencies are empty, this will only run once. + // An empty tuple is always equal to an empty tuple. + use_effect(cx, (), |()| async move { + println!("Hello, World!"); + }); + let name = name.get().clone().unwrap_or("Loading...".to_string()); render!( From 39bce476aedc99f431114e667aa430bd0938d605 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 13 Jul 2023 19:06:43 +0200 Subject: [PATCH 10/23] tweaks --- docs/guide/src/en/async/use_effect.md | 2 +- examples/test.rs | 63 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 examples/test.rs diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index f509921fc..9dc5a8739 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -1,6 +1,6 @@ # UseEffect -[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) let's you run a callback that returns a future, only it's [dependencies](#dependencies) change. This is useful to syncrhonize with external events. +[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) lets you run a callback that returns a future, which will be re-run when it's [dependencies](#dependencies) change. This is useful to syncrhonize with external events. ## Dependencies diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 000000000..546934481 --- /dev/null +++ b/examples/test.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; +use dioxus_router::*; + +fn main() { + // init debug tool for WebAssembly + wasm_logger::init(wasm_logger::Config::default()); + console_error_panic_hook::set_once(); + + dioxus_web::launch(app); +} + +fn Works1(cx: Scope) -> Element { + render!( + p { + "this is 1" + } + a { + href: "#section", + "section" + } + Link { + to: "/2", + p { + "go to 2" + } + } + p { + "{\"AAAA\n\".repeat(999)}" + } + h2 { + id: "section", + "section" + } + ) +} + +fn Works2(cx: Scope) -> Element { + render!( + p { + "this is 2" + Link { + to: "/", + p { + "go to 1" + } + } + ) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! ( + Router { + Route { + to: "/", + Works1 {} + } + Route { + to: "/2", + Works2 {} + } + } + )) +} \ No newline at end of file From f7dd4190438c3f25fc00502037a1d21d62368002 Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 13 Jul 2023 22:37:05 +0200 Subject: [PATCH 11/23] update --- docs/guide/src/en/interactivity/memoization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/en/interactivity/memoization.md b/docs/guide/src/en/interactivity/memoization.md index 72a709bcc..32ec37ce5 100644 --- a/docs/guide/src/en/interactivity/memoization.md +++ b/docs/guide/src/en/interactivity/memoization.md @@ -1,6 +1,6 @@ # Memoization -[`use_memo`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_memo.html) let's you memorize values and therefore save computation time. This is useful for expensive calculations. +[`use_memo`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_memo.html) let's you memorize values and thus save computation time. This is useful for expensive calculations. ```rust, no_run #[inline_props] From c07006e9a21d2dd60bd0f0e44fbd7090a2a9ce0c Mon Sep 17 00:00:00 2001 From: marc2332 Date: Thu, 13 Jul 2023 22:43:46 +0200 Subject: [PATCH 12/23] :bowtie: --- examples/test.rs | 63 ------------------------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 examples/test.rs diff --git a/examples/test.rs b/examples/test.rs deleted file mode 100644 index 546934481..000000000 --- a/examples/test.rs +++ /dev/null @@ -1,63 +0,0 @@ -use dioxus::prelude::*; -use dioxus_router::*; - -fn main() { - // init debug tool for WebAssembly - wasm_logger::init(wasm_logger::Config::default()); - console_error_panic_hook::set_once(); - - dioxus_web::launch(app); -} - -fn Works1(cx: Scope) -> Element { - render!( - p { - "this is 1" - } - a { - href: "#section", - "section" - } - Link { - to: "/2", - p { - "go to 2" - } - } - p { - "{\"AAAA\n\".repeat(999)}" - } - h2 { - id: "section", - "section" - } - ) -} - -fn Works2(cx: Scope) -> Element { - render!( - p { - "this is 2" - Link { - to: "/", - p { - "go to 1" - } - } - ) -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! ( - Router { - Route { - to: "/", - Works1 {} - } - Route { - to: "/2", - Works2 {} - } - } - )) -} \ No newline at end of file From 17ae835f9e03936033dce0bc9a545a3fbe817ed1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 14:14:51 -0700 Subject: [PATCH 13/23] Try pinning ubuntu version to fix CI --- .github/workflows/playwright.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 604562f6a..84891f403 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -8,7 +8,7 @@ jobs: test: if: github.event.pull_request.draft == false timeout-minutes: 60 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 From f2cd6b8af65fc4c8f1e9f109884a2e8696986f15 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 14:54:21 -0700 Subject: [PATCH 14/23] Disable LTO on CLI --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d69a74828..b7a6e96e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,8 +115,3 @@ fern = { version = "0.6.0", features = ["colored"] } thiserror = "1.0.30" env_logger = "0.10.0" simple_logger = "4.0.0" - -[profile.release] -opt-level = 3 -lto = true -debug = true From cad19fb59f003d4f16293db3ca8937f7df74cde8 Mon Sep 17 00:00:00 2001 From: Brian Donovan <1938+eventualbuddha@users.noreply.github.com> Date: Fri, 14 Jul 2023 18:38:11 -0400 Subject: [PATCH 15/23] docs: fix typo in `use_effect` description --- docs/guide/src/en/async/use_effect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/en/async/use_effect.md b/docs/guide/src/en/async/use_effect.md index 9dc5a8739..7c20e3589 100644 --- a/docs/guide/src/en/async/use_effect.md +++ b/docs/guide/src/en/async/use_effect.md @@ -1,6 +1,6 @@ # UseEffect -[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) lets you run a callback that returns a future, which will be re-run when it's [dependencies](#dependencies) change. This is useful to syncrhonize with external events. +[`use_effect`](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) lets you run a callback that returns a future, which will be re-run when its [dependencies](#dependencies) change. This is useful to syncrhonize with external events. ## Dependencies From cbd88bbcc3e15a9e8d6b0543874a6349434693e9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 16:15:20 -0700 Subject: [PATCH 16/23] Rip out async components --- examples/dog_app.rs | 12 ++-- packages/core/src/any_props.rs | 28 ++++----- packages/core/src/create.rs | 19 ------ packages/core/src/diff.rs | 19 +----- packages/core/src/nodes.rs | 30 ---------- packages/core/src/properties.rs | 2 +- packages/core/src/scheduler/mod.rs | 7 --- packages/core/src/scheduler/suspense.rs | 20 ------- packages/core/src/scheduler/wait.rs | 79 +------------------------ packages/core/src/scope_arena.rs | 66 +-------------------- packages/core/src/scopes.rs | 6 +- packages/core/src/virtual_dom.rs | 11 +--- packages/core/tests/suspense.rs | 59 +++++++++--------- packages/hooks/src/usefuture.rs | 4 ++ 14 files changed, 62 insertions(+), 300 deletions(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 107251cd7..510550b99 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -10,7 +10,7 @@ struct ListBreeds { message: HashMap>, } -async fn app_root(cx: Scope<'_>) -> Element { +fn app_root(cx: Scope<'_>) -> Element { let breed = use_state(cx, || "deerhound".to_string()); let breeds = use_future!(cx, || async move { @@ -21,13 +21,13 @@ async fn app_root(cx: Scope<'_>) -> Element { .await }); - match breeds.await { - Ok(breeds) => cx.render(rsx! { + match breeds.suspend()? { + Ok(breed_list) => cx.render(rsx! { div { height: "500px", h1 { "Select a dog breed!" } div { display: "flex", ul { flex: "50%", - for cur_breed in breeds.message.keys().take(10) { + for cur_breed in breed_list.message.keys().take(10) { li { key: "{cur_breed}", button { onclick: move |_| breed.set(cur_breed.clone()), @@ -50,7 +50,7 @@ struct DogApi { } #[inline_props] -async fn breed_pic(cx: Scope, breed: String) -> Element { +fn breed_pic(cx: Scope, breed: String) -> Element { let fut = use_future!(cx, |breed| async move { reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) .await @@ -59,7 +59,7 @@ async fn breed_pic(cx: Scope, breed: String) -> Element { .await }); - match fut.await { + match fut.suspend()? { Ok(resp) => render! { div { button { diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 7014a2771..e3a3bdaef 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,8 +1,8 @@ -use std::{marker::PhantomData, panic::AssertUnwindSafe}; +use std::{ panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, - nodes::{ComponentReturn, RenderReturn}, + nodes::RenderReturn, scopes::{Scope, ScopeState}, Element, }; @@ -18,19 +18,15 @@ pub(crate) unsafe trait AnyProps<'a> { unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; } -pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> { - pub render_fn: fn(Scope<'a, P>) -> F, +pub(crate) struct VProps<'a, P> { + pub render_fn: fn(Scope<'a, P>) -> Element<'a>, pub memo: unsafe fn(&P, &P) -> bool, pub props: P, - _marker: PhantomData, } -impl<'a, P, A, F> VProps<'a, P, A, F> -where - F: ComponentReturn<'a, A>, -{ +impl<'a, P> VProps<'a, P> { pub(crate) fn new( - render_fn: fn(Scope<'a, P>) -> F, + render_fn: fn(Scope<'a, P>) -> Element<'a>, memo: unsafe fn(&P, &P) -> bool, props: P, ) -> Self { @@ -38,15 +34,11 @@ where render_fn, memo, props, - _marker: PhantomData, } } } -unsafe impl<'a, P, A, F> AnyProps<'a> for VProps<'a, P, A, F> -where - F: ComponentReturn<'a, A>, -{ +unsafe impl<'a, P> AnyProps<'a> for VProps<'a, P> { fn props_ptr(&self) -> *const () { &self.props as *const _ as *const () } @@ -69,12 +61,12 @@ where scope: cx, }); - (self.render_fn)(scope).into_return(cx) + (self.render_fn)(scope) })); match res { - Ok(e) => e, - Err(_) => RenderReturn::default(), + Ok(Some(e)) => RenderReturn::Ready(e), + _ => RenderReturn::default(), } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 80561e937..d31e5ab47 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -514,7 +514,6 @@ impl<'b> VirtualDom { match unsafe { self.run_scope(scope).extend_lifetime_ref() } { Ready(t) => self.mount_component(scope, template, t, idx), Aborted(t) => self.mount_aborted(template, t), - Pending(_) => self.mount_async(template, idx, scope), } } @@ -591,24 +590,6 @@ impl<'b> VirtualDom { 1 } - /// Take the rendered nodes from a component and handle them if they were async - /// - /// IE simply assign an ID to the placeholder - fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize { - let new_id = self.next_element(template, template.template.get().node_paths[idx]); - - // Set the placeholder of the scope - self.scopes[scope].placeholder.set(Some(new_id)); - - // Since the placeholder is already in the DOM, we don't create any new nodes - self.mutations.push(AssignId { - id: new_id, - path: &template.template.get().node_paths[idx][1..], - }); - - 0 - } - fn set_slot( &mut self, template: &'b VNode<'b>, diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index a8b913440..c06e46d3f 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -30,7 +30,7 @@ impl<'b> VirtualDom { .try_load_node() .expect("Call rebuild before diffing"); - use RenderReturn::{Aborted, Pending, Ready}; + use RenderReturn::{Aborted, Ready}; match (old, new) { // Normal pathway @@ -42,29 +42,14 @@ impl<'b> VirtualDom { // Just move over the placeholder (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), - // Becomes async, do nothing while we wait - (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), - // Placeholder becomes something // We should also clear the error now (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), - - (Aborted(_), Pending(_)) => todo!("async should not resolve here"), - (Pending(_), Ready(_)) => todo!("async should not resolve here"), - (Pending(_), Aborted(_)) => todo!("async should not resolve here"), - (Pending(_), Pending(_)) => { - // All suspense should resolve before we diff it again - panic!("Should not roll from suspense to suspense."); - } }; } self.scope_stack.pop(); } - fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) { - // - } - fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) { let id = self.next_null(); p.id.set(Some(id)); @@ -735,7 +720,6 @@ impl<'b> VirtualDom { match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { RenderReturn::Ready(node) => self.push_all_real_nodes(node), RenderReturn::Aborted(_node) => todo!(), - _ => todo!(), } } } @@ -937,7 +921,6 @@ impl<'b> VirtualDom { match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } { RenderReturn::Ready(t) => self.remove_node(t, gen_muts), RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts), - _ => todo!(), }; // Restore the props back to the vcomponent in case it gets rendered again diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dabb479cb..209c37c77 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -7,7 +7,6 @@ use std::{ any::{Any, TypeId}, cell::{Cell, RefCell, UnsafeCell}, fmt::{Arguments, Debug}, - future::Future, }; pub type TemplateId = &'static str; @@ -28,9 +27,6 @@ pub enum RenderReturn<'a> { /// In its place we've produced a placeholder to locate its spot in the dom when /// it recovers. Aborted(VPlaceholder), - - /// An ongoing future that will resolve to a [`Element`] - Pending(BumpBox<'a, dyn Future> + 'a>), } impl<'a> Default for RenderReturn<'a> { @@ -688,32 +684,6 @@ impl AnyValue for T { } } -#[doc(hidden)] -pub trait ComponentReturn<'a, A = ()> { - fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; -} - -impl<'a> ComponentReturn<'a> for Element<'a> { - fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - match self { - Some(node) => RenderReturn::Ready(node), - None => RenderReturn::default(), - } - } -} - -#[doc(hidden)] -pub struct AsyncMarker; -impl<'a, F> ComponentReturn<'a, AsyncMarker> for F -where - F: Future> + 'a, -{ - fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { - let f: &mut dyn Future> = cx.bump().alloc(self); - RenderReturn::Pending(unsafe { BumpBox::from_raw(f) }) - } -} - impl<'a> RenderReturn<'a> { pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> { unsafe { std::mem::transmute(self) } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index f8be8a0dd..834b0cabd 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -70,6 +70,6 @@ impl EmptyBuilder { /// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern /// to initialize a component's props. -pub fn fc_to_builder<'a, A, T: Properties + 'a>(_: fn(Scope<'a, T>) -> A) -> T::Builder { +pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder { T::builder() } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index 8fb476fbe..f5c540dd3 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -18,9 +18,6 @@ pub(crate) enum SchedulerMsg { /// A task has woken and needs to be progressed TaskNotified(TaskId), - - /// A task has woken and needs to be progressed - SuspenseNotified(SuspenseId), } use std::{cell::RefCell, rc::Rc}; @@ -30,9 +27,6 @@ pub(crate) struct Scheduler { /// Tasks created with cx.spawn pub tasks: RefCell>, - - /// Async components - pub leaves: RefCell>, } impl Scheduler { @@ -40,7 +34,6 @@ impl Scheduler { Rc::new(Scheduler { sender, tasks: RefCell::new(Slab::new()), - leaves: RefCell::new(Slab::new()), }) } } diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 39e404198..ad3d525b7 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -36,23 +36,3 @@ impl SuspenseContext { } } } - -pub(crate) struct SuspenseLeaf { - pub(crate) scope_id: ScopeId, - pub(crate) notified: Cell, - pub(crate) task: *mut dyn Future>, - pub(crate) waker: Waker, -} - -pub struct SuspenseHandle { - pub(crate) id: SuspenseId, - pub(crate) tx: futures_channel::mpsc::UnboundedSender, -} - -impl ArcWake for SuspenseHandle { - fn wake_by_ref(arc_self: &Arc) { - _ = arc_self - .tx - .unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id)); - } -} diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 77bea3a18..4e56f50f0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,16 +1,5 @@ -use futures_util::FutureExt; -use std::{ - rc::Rc, - task::{Context, Poll}, -}; - -use crate::{ - innerlude::{Mutation, Mutations, SuspenseContext}, - nodes::RenderReturn, - ScopeId, TaskId, VNode, VirtualDom, -}; - -use super::SuspenseId; +use crate::{innerlude::SuspenseContext, ScopeId, TaskId, VirtualDom}; +use std::{rc::Rc, task::Context}; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -44,68 +33,4 @@ impl VirtualDom { .consume_context::>() .unwrap() } - - pub(crate) fn handle_suspense_wakeup(&mut self, id: SuspenseId) { - let leaves = self.scheduler.leaves.borrow_mut(); - let leaf = leaves.get(id.0).unwrap(); - - let scope_id = leaf.scope_id; - - // todo: cache the waker - let mut cx = Context::from_waker(&leaf.waker); - - // Safety: the future is always pinned to the bump arena - let mut pinned = unsafe { std::pin::Pin::new_unchecked(&mut *leaf.task) }; - let as_pinned_mut = &mut pinned; - - // the component finished rendering and gave us nodes - // we should attach them to that component and then render its children - // continue rendering the tree until we hit yet another suspended component - if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - let fiber = self.acquire_suspense_boundary(leaf.scope_id); - - let scope = &self.scopes[scope_id]; - let arena = scope.current_frame(); - - let ret = arena.bump().alloc(match new_nodes { - Some(new) => RenderReturn::Ready(new), - None => RenderReturn::default(), - }); - - arena.node.set(ret); - - fiber.waiting_on.borrow_mut().remove(&id); - - if let RenderReturn::Ready(template) = ret { - let mutations_ref = &mut fiber.mutations.borrow_mut(); - let mutations = &mut **mutations_ref; - let template: &VNode = unsafe { std::mem::transmute(template) }; - let mutations: &mut Mutations = unsafe { std::mem::transmute(mutations) }; - - std::mem::swap(&mut self.mutations, mutations); - - let place_holder_id = scope.placeholder.get().unwrap(); - self.scope_stack.push(scope_id); - - drop(leaves); - - let created = self.create(template); - self.scope_stack.pop(); - mutations.push(Mutation::ReplaceWith { - id: place_holder_id, - m: created, - }); - - for leaf in self.collected_leaves.drain(..) { - fiber.waiting_on.borrow_mut().insert(leaf); - } - - std::mem::swap(&mut self.mutations, mutations); - - if fiber.waiting_on.borrow().is_empty() { - self.finished_fibers.push(fiber.id); - } - } - } - } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index a8a7319aa..96bf8e08b 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -2,18 +2,10 @@ use crate::{ any_props::AnyProps, bump_frame::BumpFrame, innerlude::DirtyScope, - innerlude::{SuspenseHandle, SuspenseId, SuspenseLeaf}, nodes::RenderReturn, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; -use futures_util::FutureExt; -use std::{ - mem, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; impl VirtualDom { pub(super) fn new_scope( @@ -58,7 +50,7 @@ impl VirtualDom { // Remove all the outdated listeners self.ensure_drop_safety(scope_id); - let mut new_nodes = unsafe { + let new_nodes = unsafe { self.scopes[scope_id].previous_frame().bump_mut().reset(); let scope = &self.scopes[scope_id]; @@ -67,65 +59,11 @@ impl VirtualDom { // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); - let props: &dyn AnyProps = mem::transmute(props); + let props: &dyn AnyProps = std::mem::transmute(props); props.render(scope).extend_lifetime() }; - // immediately resolve futures that can be resolved - if let RenderReturn::Pending(task) = &mut new_nodes { - let mut leaves = self.scheduler.leaves.borrow_mut(); - - let entry = leaves.vacant_entry(); - let suspense_id = SuspenseId(entry.key()); - - let leaf = SuspenseLeaf { - scope_id, - task: task.as_mut(), - notified: Default::default(), - waker: futures_util::task::waker(Arc::new(SuspenseHandle { - id: suspense_id, - tx: self.scheduler.sender.clone(), - })), - }; - - let mut cx = Context::from_waker(&leaf.waker); - - // safety: the task is already pinned in the bump arena - let mut pinned = unsafe { Pin::new_unchecked(task.as_mut()) }; - - // Keep polling until either we get a value or the future is not ready - loop { - match pinned.poll_unpin(&mut cx) { - // If nodes are produced, then set it and we can break - Poll::Ready(nodes) => { - new_nodes = match nodes { - Some(nodes) => RenderReturn::Ready(nodes), - None => RenderReturn::default(), - }; - - break; - } - - // If no nodes are produced but the future woke up immediately, then try polling it again - // This circumvents things like yield_now, but is important is important when rendering - // components that are just a stream of immediately ready futures - _ if leaf.notified.get() => { - leaf.notified.set(false); - continue; - } - - // If no nodes are produced, then we need to wait for the future to be woken up - // Insert the future into fiber leaves and break - _ => { - entry.insert(leaf); - self.collected_leaves.push(suspense_id); - break; - } - }; - } - }; - let scope = &self.scopes[scope_id]; // We write on top of the previous frame and then make it the current by pushing the generation forward diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ff94c456d..02b9bb65a 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -6,7 +6,7 @@ use crate::{ innerlude::{DynamicNode, EventHandler, VComponent, VText}, innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, - nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, + nodes::{IntoAttributeValue, IntoDynNode, RenderReturn}, AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId, }; use bumpalo::{boxed::Box as BumpBox, Bump}; @@ -574,9 +574,9 @@ impl<'src> ScopeState { /// fn(Scope) -> Element; /// async fn(Scope>) -> Element; /// ``` - pub fn component>( + pub fn component

( &'src self, - component: fn(Scope<'src, P>) -> F, + component: fn(Scope<'src, P>) -> Element<'src>, props: P, fn_name: &'static str, ) -> DynamicNode<'src> diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b118cd59a..96c935d1b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -335,7 +335,8 @@ impl VirtualDom { /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is /// ready to be rendered. pub fn has_suspended_work(&self) -> bool { - !self.scheduler.leaves.borrow().is_empty() + todo!() + // !self.scheduler.leaves.borrow().is_empty() } /// Call a listener inside the VirtualDom with data from outside the VirtualDom. @@ -485,7 +486,6 @@ impl VirtualDom { Some(msg) => match msg { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), - SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), }, // If they're not ready, then we should wait for them to be ready @@ -513,7 +513,6 @@ impl VirtualDom { match msg { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task), - SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id), } } } @@ -574,7 +573,6 @@ impl VirtualDom { } // If an error occurs, we should try to render the default error component and context where the error occured RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"), - RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"), } self.finalize() @@ -680,11 +678,6 @@ impl VirtualDom { continue; } - // If there's no pending suspense, then we have no reason to wait for anything - if self.scheduler.leaves.borrow().is_empty() { - return self.finalize(); - } - // Poll the suspense leaves in the meantime let mut work = self.wait_for_work(); diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 06bb7b1a4..708494dd0 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -59,42 +59,45 @@ fn app(cx: Scope) -> Element { } fn suspense_boundary(cx: Scope) -> Element { - cx.use_hook(|| { - cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); - }); + todo!() + // cx.use_hook(|| { + // cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); + // }); - // Ensure the right types are found - cx.has_context::>().unwrap(); + // // Ensure the right types are found + // cx.has_context::>().unwrap(); - cx.render(rsx!(async_child {})) + // cx.render(rsx!(async_child {})) } -async fn async_child(cx: Scope<'_>) -> Element { - use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; - cx.render(rsx!(async_text {})) +fn async_child(cx: Scope<'_>) -> Element { + todo!() + // use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; + // cx.render(rsx!(async_text {})) } -async fn async_text(cx: Scope<'_>) -> Element { - let username = use_future!(cx, || async { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - "async child 1" - }); +fn async_text(cx: Scope<'_>) -> Element { + todo!() + // let username = use_future!(cx, || async { + // tokio::time::sleep(std::time::Duration::from_secs(1)).await; + // "async child 1" + // }); - let age = use_future!(cx, || async { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - 1234 - }); + // let age = use_future!(cx, || async { + // tokio::time::sleep(std::time::Duration::from_secs(2)).await; + // 1234 + // }); - let (_user, _age) = use_future!(cx, || async { - tokio::join!( - tokio::time::sleep(std::time::Duration::from_secs(1)), - tokio::time::sleep(std::time::Duration::from_secs(2)) - ); - ("async child 1", 1234) - }) - .await; + // let (_user, _age) = use_future!(cx, || async { + // tokio::join!( + // tokio::time::sleep(std::time::Duration::from_secs(1)), + // tokio::time::sleep(std::time::Duration::from_secs(2)) + // ); + // ("async child 1", 1234) + // }) + // .await; - let (username, age) = tokio::join!(username.into_future(), age.into_future()); + // let (username, age) = tokio::join!(username.into_future(), age.into_future()); - cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) + // cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) } diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 18bf50310..9d39e82de 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -167,6 +167,10 @@ impl UseFuture { (Some(_), None) => UseFutureState::Pending, } } + + pub fn suspend(&self) -> Option<&T> { + todo!() + } } impl<'a, T> IntoFuture for &'a UseFuture { From d583e1c7506f597830095f40204d063c6c71b850 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 16:34:49 -0700 Subject: [PATCH 17/23] Fix clippy --- examples/shared_state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/shared_state.rs b/examples/shared_state.rs index 2d5f6f125..d129f75d1 100644 --- a/examples/shared_state.rs +++ b/examples/shared_state.rs @@ -54,7 +54,7 @@ pub fn App(cx: Scope) -> Element { fn DataEditor(cx: Scope, id: usize) -> Element { let cool_data = use_shared_state::(cx).unwrap().read(); - let my_data = &cool_data.view(&id).unwrap(); + let my_data = &cool_data.view(id).unwrap(); render!(p { "{my_data}" @@ -68,7 +68,7 @@ fn DataView(cx: Scope, id: usize) -> Element { let oninput = |e: FormEvent| cool_data.write().set(*id, e.value.clone()); let cool_data = cool_data.read(); - let my_data = &cool_data.view(&id).unwrap(); + let my_data = &cool_data.view(id).unwrap(); render!(input { oninput: oninput, From 81075748f61a6c3e7e8f5d75400ac0a8f224d9af Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 16:56:17 -0700 Subject: [PATCH 18/23] add simple suspense methods --- packages/core/src/create.rs | 63 +------------- packages/core/src/diff.rs | 17 ++-- packages/core/src/lib.rs | 2 +- packages/core/src/scheduler/mod.rs | 2 - packages/core/src/scheduler/suspense.rs | 16 ++-- packages/core/src/scheduler/wait.rs | 10 +-- packages/core/src/scope_arena.rs | 7 +- packages/core/src/scopes.rs | 9 +- packages/core/src/virtual_dom.rs | 108 +++++------------------- packages/core/tests/safety.rs | 5 -- packages/core/tests/suspense.rs | 2 +- 11 files changed, 52 insertions(+), 189 deletions(-) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index d31e5ab47..8efdf13e4 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -5,10 +5,9 @@ use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext, Template}; +use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, Template}; use std::cell::Cell; use std::iter::Peekable; -use std::rc::Rc; use TemplateNode::*; #[cfg(debug_assertions)] @@ -445,7 +444,7 @@ impl<'b> VirtualDom { match node { Text(text) => self.create_dynamic_text(template, text, idx), Placeholder(place) => self.create_placeholder(place, template, idx), - Component(component) => self.create_component_node(template, component, idx), + Component(component) => self.create_component_node(template, component), Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), } } @@ -502,7 +501,6 @@ impl<'b> VirtualDom { &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, - idx: usize, ) -> usize { use RenderReturn::*; @@ -512,7 +510,8 @@ impl<'b> VirtualDom { component.scope.set(Some(scope)); match unsafe { self.run_scope(scope).extend_lifetime_ref() } { - Ready(t) => self.mount_component(scope, template, t, idx), + // Create the component's root element + Ready(t) => self.create_scope(scope, t), Aborted(t) => self.mount_aborted(template, t), } } @@ -529,60 +528,6 @@ impl<'b> VirtualDom { .unwrap_or_else(|| component.scope.get().unwrap()) } - fn mount_component( - &mut self, - scope: ScopeId, - parent: &'b VNode<'b>, - new: &'b VNode<'b>, - idx: usize, - ) -> usize { - // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary - // is encountered - let mutations_to_this_point = self.mutations.edits.len(); - - // Create the component's root element - let created = self.create_scope(scope, new); - - // If there are no suspense leaves below us, then just don't bother checking anything suspense related - if self.collected_leaves.is_empty() { - return created; - } - - // If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense - let boundary = match self.scopes[scope].has_context::>() { - Some(boundary) => boundary, - _ => return created, - }; - - // Since this is a boundary, use its placeholder within the template as the placeholder for the suspense tree - let new_id = self.next_element(new, parent.template.get().node_paths[idx]); - - // Now connect everything to the boundary - self.scopes[scope].placeholder.set(Some(new_id)); - - // This involves breaking off the mutations to this point, and then creating a new placeholder for the boundary - // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately - let split_off = unsafe { - std::mem::transmute::, Vec>( - self.mutations.edits.split_off(mutations_to_this_point), - ) - }; - boundary.mutations.borrow_mut().edits.extend(split_off); - boundary.created_on_stack.set(created); - boundary - .waiting_on - .borrow_mut() - .extend(self.collected_leaves.drain(..)); - - // Now assign the placeholder in the DOM - self.mutations.push(AssignId { - id: new_id, - path: &parent.template.get().node_paths[idx][1..], - }); - - 0 - } - fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { let id = self.next_element(parent, &[]); self.mutations.push(Mutation::CreatePlaceholder { id }); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index c06e46d3f..cfda18505 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -124,9 +124,8 @@ impl<'b> VirtualDom { .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) - .enumerate() - .for_each(|(idx, (left_node, right_node))| { - self.diff_dynamic_node(left_node, right_node, right_template, idx); + .for_each(|(left_node, right_node)| { + self.diff_dynamic_node(left_node, right_node, right_template); }); // Make sure the roots get transferred over while we're here @@ -145,13 +144,12 @@ impl<'b> VirtualDom { left_node: &'b DynamicNode<'b>, right_node: &'b DynamicNode<'b>, node: &'b VNode<'b>, - idx: usize, ) { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right, node), (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), - (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), + (Component(left), Component(right)) => self.diff_vcomponent(left, right, node), (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), @@ -175,7 +173,6 @@ impl<'b> VirtualDom { left: &'b VComponent<'b>, right: &'b VComponent<'b>, right_template: &'b VNode<'b>, - idx: usize, ) { if std::ptr::eq(left, right) { return; @@ -183,7 +180,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - return self.replace_vcomponent(right_template, right, idx, left); + return self.replace_vcomponent(right_template, right, left); } // Make sure the new vcomponent has the right scopeid associated to it @@ -220,10 +217,9 @@ impl<'b> VirtualDom { &mut self, right_template: &'b VNode<'b>, right: &'b VComponent<'b>, - idx: usize, left: &'b VComponent<'b>, ) { - let m = self.create_component_node(right_template, right, idx); + let m = self.create_component_node(right_template, right); let pre_edits = self.mutations.edits.len(); @@ -282,8 +278,7 @@ impl<'b> VirtualDom { None => self.replace(left, [right]), Some(components) => components .into_iter() - .enumerate() - .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), + .for_each(|(l, r)| self.diff_vcomponent(l, r, right)), } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 304197dab..514999242 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -72,7 +72,7 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, - Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, + Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index f5c540dd3..a13958363 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -1,11 +1,9 @@ use crate::ScopeId; use slab::Slab; -mod suspense; mod task; mod wait; -pub use suspense::*; pub use task::*; /// The type of message that can be sent to the scheduler. diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index ad3d525b7..77c2981ae 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -11,17 +11,10 @@ use std::{ collections::HashSet, }; -/// An ID representing an ongoing suspended component -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub(crate) struct SuspenseId(pub usize); - /// A boundary in the VirtualDom that captures all suspended components below it pub struct SuspenseContext { pub(crate) id: ScopeId, - pub(crate) waiting_on: RefCell>, - pub(crate) mutations: RefCell>, - pub(crate) placeholder: Cell>, - pub(crate) created_on_stack: Cell, + pub(crate) waiting_on: RefCell>, } impl SuspenseContext { @@ -30,9 +23,10 @@ impl SuspenseContext { Self { id, waiting_on: Default::default(), - mutations: RefCell::new(Mutations::default()), - placeholder: Cell::new(None), - created_on_stack: Cell::new(0), } } + + pub fn mark_suspend(&self, id: ScopeId) { + self.waiting_on.borrow_mut().insert(id); + } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 4e56f50f0..0a7dce207 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -1,5 +1,5 @@ -use crate::{innerlude::SuspenseContext, ScopeId, TaskId, VirtualDom}; -use std::{rc::Rc, task::Context}; +use crate::{TaskId, VirtualDom}; +use std::task::Context; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -27,10 +27,4 @@ impl VirtualDom { tasks.try_remove(id.0); } } - - pub(crate) fn acquire_suspense_boundary(&self, id: ScopeId) -> Rc { - self.scopes[id] - .consume_context::>() - .unwrap() - } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 96bf8e08b..306afe16b 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -25,10 +25,10 @@ impl VirtualDom { name, props: Some(props), tasks: self.scheduler.clone(), - placeholder: Default::default(), node_arena_1: BumpFrame::new(0), node_arena_2: BumpFrame::new(0), spawned_tasks: Default::default(), + suspended: Default::default(), render_cnt: Default::default(), hooks: Default::default(), hook_idx: Default::default(), @@ -54,6 +54,7 @@ impl VirtualDom { self.scopes[scope_id].previous_frame().bump_mut().reset(); let scope = &self.scopes[scope_id]; + scope.suspended.set(false); scope.hook_idx.set(0); @@ -82,6 +83,10 @@ impl VirtualDom { id: scope.id, }); + if scope.suspended.get() { + self.suspended_scopes.insert(scope.id); + } + // rebind the lifetime now that its stored internally unsafe { allocated.extend_lifetime_ref() } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 02b9bb65a..7bc26fb02 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,7 +1,6 @@ use crate::{ any_props::AnyProps, any_props::VProps, - arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, @@ -169,6 +168,7 @@ pub struct ScopeState { pub(crate) id: ScopeId, pub(crate) height: u32, + pub(crate) suspended: Cell, pub(crate) hooks: RefCell>>>, pub(crate) hook_idx: Cell, @@ -182,7 +182,6 @@ pub struct ScopeState { pub(crate) attributes_to_drop: RefCell>>, pub(crate) props: Option>>, - pub(crate) placeholder: Cell>, } impl<'src> ScopeState { @@ -655,6 +654,12 @@ impl<'src> ScopeState { None } + /// Mark this component as suspended and then return None + pub fn suspend(&self) -> Option<()> { + self.suspended.set(true); + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 96c935d1b..1f056cfae 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,14 +9,13 @@ use crate::{ mutations::Mutation, nodes::RenderReturn, nodes::{Template, TemplateId}, - scheduler::SuspenseId, scopes::{ScopeId, ScopeState}, - AttributeValue, Element, Event, Scope, SuspenseContext, + AttributeValue, Element, Event, Scope, }; use futures_util::{pin_mut, StreamExt}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use slab::Slab; -use std::{any::Any, borrow::BorrowMut, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; +use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc}; /// A virtual node system that progresses user events and diffs UI trees. /// @@ -186,11 +185,9 @@ pub struct VirtualDom { // While diffing we need some sort of way of breaking off a stream of suspended mutations. pub(crate) scope_stack: Vec, - pub(crate) collected_leaves: Vec, - // Whenever a suspense tree is finished, we push its boundary onto this stack. - // When "render_with_deadline" is called, we pop the stack and return the mutations - pub(crate) finished_fibers: Vec, + // Currently suspended scopes + pub(crate) suspended_scopes: FxHashSet, pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, @@ -262,8 +259,7 @@ impl VirtualDom { elements: Default::default(), scope_stack: Vec::new(), dirty_scopes: BTreeSet::new(), - collected_leaves: Vec::new(), - finished_fibers: Vec::new(), + suspended_scopes: FxHashSet::default(), mutations: Mutations::default(), }; @@ -272,12 +268,6 @@ impl VirtualDom { "app", ); - // The root component is always a suspense boundary for any async children - // This could be unexpected, so we might rethink this behavior later - // - // We *could* just panic if the suspense boundary is not found - root.provide_context(Rc::new(SuspenseContext::new(ScopeId(0)))); - // Unlike react, we provide a default error boundary that just renders the error as a string root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0)))); @@ -319,26 +309,6 @@ impl VirtualDom { } } - /// Determine whether or not a scope is currently in a suspended state - /// - /// This does not mean the scope is waiting on its own futures, just that the tree that the scope exists in is - /// currently suspended. - pub fn is_scope_suspended(&self, id: ScopeId) -> bool { - !self.scopes[id] - .consume_context::>() - .unwrap() - .waiting_on - .borrow() - .is_empty() - } - - /// Determine if the tree is at all suspended. Used by SSR and other outside mechanisms to determine if the tree is - /// ready to be rendered. - pub fn has_suspended_work(&self) -> bool { - todo!() - // !self.scheduler.leaves.borrow().is_empty() - } - /// Call a listener inside the VirtualDom with data from outside the VirtualDom. /// /// This method will identify the appropriate element. The data must match up with the listener delcared. Note that @@ -495,7 +465,7 @@ impl VirtualDom { Ok(None) => return, Err(_) => { // If we have any dirty scopes, or finished fiber trees then we should exit - if !self.dirty_scopes.is_empty() || !self.finished_fibers.is_empty() { + if !self.dirty_scopes.is_empty() || !self.suspended_scopes.is_empty() { return; } @@ -596,6 +566,19 @@ impl VirtualDom { } } + /// Render the virtual dom, waiting for all suspense to be finished + /// + /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content + pub async fn wait_for_suspense(&mut self) { + loop { + if self.suspended_scopes.is_empty() { + break; + } + + self.wait_for_work().await; + } + } + /// Render what you can given the timeline and then move on /// /// It's generally a good idea to put some sort of limit on the suspense process in case a future is having issues. @@ -607,26 +590,6 @@ impl VirtualDom { self.process_events(); loop { - // first, unload any complete suspense trees - for finished_fiber in self.finished_fibers.drain(..) { - let scope = &self.scopes[finished_fiber]; - let context = scope.has_context::>().unwrap(); - - self.mutations - .templates - .append(&mut context.mutations.borrow_mut().templates); - - self.mutations - .edits - .append(&mut context.mutations.borrow_mut().edits); - - // TODO: count how many nodes are on the stack? - self.mutations.push(Mutation::ReplaceWith { - id: context.placeholder.get().unwrap(), - m: 1, - }) - } - // Next, diff any dirty scopes // We choose not to poll the deadline since we complete pretty quickly anyways if let Some(dirty) = self.dirty_scopes.iter().next().cloned() { @@ -637,40 +600,9 @@ impl VirtualDom { continue; } - // if the scope is currently suspended, then we should skip it, ignoring any tasks calling for an update - if self.is_scope_suspended(dirty.id) { - continue; - } - - // Save the current mutations length so we can split them into boundary - let mutations_to_this_point = self.mutations.edits.len(); - // Run the scope and get the mutations self.run_scope(dirty.id); self.diff_scope(dirty.id); - - // If suspended leaves are present, then we should find the boundary for this scope and attach things - // No placeholder necessary since this is a diff - if !self.collected_leaves.is_empty() { - let mut boundary = self.scopes[dirty.id] - .consume_context::>() - .unwrap(); - - let boundary_mut = boundary.borrow_mut(); - - // Attach mutations - boundary_mut - .mutations - .borrow_mut() - .edits - .extend(self.mutations.edits.split_off(mutations_to_this_point)); - - // Attach suspended leaves - boundary - .waiting_on - .borrow_mut() - .extend(self.collected_leaves.drain(..)); - } } // If there's more work, then just continue, plenty of work to do diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 8c125f4c6..e5763674d 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -3,7 +3,6 @@ use std::rc::Rc; use dioxus::prelude::*; -use dioxus_core::SuspenseContext; /// Ensure no issues with not calling rebuild #[test] @@ -17,8 +16,4 @@ fn root_node_isnt_null() { // The height should be 0 assert_eq!(scope.height(), 0); - - // There should be a default suspense context - // todo: there should also be a default error boundary - assert!(scope.has_context::>().is_some()); } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 708494dd0..2b90a37da 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,5 +1,5 @@ use dioxus::core::ElementId; -use dioxus::core::{Mutation::*, SuspenseContext}; +use dioxus::core::Mutation::*; use dioxus::prelude::*; use std::future::IntoFuture; use std::rc::Rc; From f0128a3150faede0ca6f9f610ff45995a6bca4f9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:12:07 -0700 Subject: [PATCH 19/23] implement suspense using simpler approach --- packages/core/Cargo.toml | 1 + packages/core/src/scope_arena.rs | 2 + packages/core/src/scopes.rs | 2 +- packages/core/src/virtual_dom.rs | 4 +- packages/core/tests/suspense.rs | 101 ++++++------------------------- 5 files changed, 27 insertions(+), 83 deletions(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 0b668f43d..a077a7379 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -40,6 +40,7 @@ tokio = { workspace = true, features = ["full"] } dioxus = { workspace = true } pretty_assertions = "1.3.0" rand = "0.8.5" +dioxus-ssr = { workspace = true } [features] default = [] diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 306afe16b..39b39f8c4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -85,6 +85,8 @@ impl VirtualDom { if scope.suspended.get() { self.suspended_scopes.insert(scope.id); + } else if !self.suspended_scopes.is_empty() { + _ = self.suspended_scopes.remove(&scope.id); } // rebind the lifetime now that its stored internally diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 7bc26fb02..8d980222c 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -655,7 +655,7 @@ impl<'src> ScopeState { } /// Mark this component as suspended and then return None - pub fn suspend(&self) -> Option<()> { + pub fn suspend(&self) -> Option { self.suspended.set(true); None } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1f056cfae..31a1b5912 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -572,10 +572,12 @@ impl VirtualDom { pub async fn wait_for_suspense(&mut self) { loop { if self.suspended_scopes.is_empty() { - break; + return; } self.wait_for_work().await; + + _ = self.render_immediate(); } } diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 2b90a37da..2a64dc53c 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,103 +1,42 @@ -use dioxus::core::ElementId; use dioxus::core::Mutation::*; use dioxus::prelude::*; use std::future::IntoFuture; use std::rc::Rc; use std::time::Duration; -#[test] -fn it_works() { +#[tokio::test] +async fn it_works() { // wait just a moment, not enough time for the boundary to resolve - tokio::runtime::Builder::new_current_thread() - .enable_time() - .build() - .unwrap() - .block_on(async { - let mut dom = VirtualDom::new(app); + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + dom.wait_for_suspense().await; + let out = dioxus_ssr::pre_render(&dom); - { - let mutations = dom.rebuild().santize(); + assert_eq!(out, "

Waiting for... child
"); - // We should at least get the top-level template in before pausing for the children - // note: we dont test template edits anymore - // assert_eq!( - // mutations.templates, - // [ - // CreateElement { name: "div" }, - // CreateStaticText { value: "Waiting for child..." }, - // CreateStaticPlaceholder, - // AppendChildren { m: 2 }, - // SaveTemplate { name: "template", m: 1 } - // ] - // ); - - // And we should load it in and assign the placeholder properly - assert_eq!( - mutations.edits, - [ - LoadTemplate { name: "template", index: 0, id: ElementId(1) }, - // hmmmmmmmmm.... with suspense how do we guarantee that IDs increase linearly? - // can we even? - AssignId { path: &[1], id: ElementId(3) }, - AppendChildren { m: 1, id: ElementId(0) }, - ] - ); - } - - dom.wait_for_work().await; - }); + dbg!(out); } fn app(cx: Scope) -> Element { cx.render(rsx!( div { - "Waiting for child..." - suspense_boundary {} + "Waiting for... " + suspended_child {} } )) } -fn suspense_boundary(cx: Scope) -> Element { - todo!() - // cx.use_hook(|| { - // cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); - // }); +fn suspended_child(cx: Scope) -> Element { + let mut val = use_state(cx, || 0); - // // Ensure the right types are found - // cx.has_context::>().unwrap(); + if **val < 3 { + let mut val = val.clone(); + cx.spawn(async move { + val += 1; + }); + return cx.suspend()?; + } - // cx.render(rsx!(async_child {})) -} - -fn async_child(cx: Scope<'_>) -> Element { - todo!() - // use_future!(cx, || tokio::time::sleep(Duration::from_millis(10))).await; - // cx.render(rsx!(async_text {})) -} - -fn async_text(cx: Scope<'_>) -> Element { - todo!() - // let username = use_future!(cx, || async { - // tokio::time::sleep(std::time::Duration::from_secs(1)).await; - // "async child 1" - // }); - - // let age = use_future!(cx, || async { - // tokio::time::sleep(std::time::Duration::from_secs(2)).await; - // 1234 - // }); - - // let (_user, _age) = use_future!(cx, || async { - // tokio::join!( - // tokio::time::sleep(std::time::Duration::from_secs(1)), - // tokio::time::sleep(std::time::Duration::from_secs(2)) - // ); - // ("async child 1", 1234) - // }) - // .await; - - // let (username, age) = tokio::join!(username.into_future(), age.into_future()); - - // cx.render(rsx!( div { "Hello! {username}, you are {age}, {_user} {_age}" } )) + render!("child") } From 0f5605b3db2a4148c67a40758df50fbcdbe4c8f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:13:49 -0700 Subject: [PATCH 20/23] Apply clippy fixes --- packages/core/tests/safety.rs | 2 +- packages/core/tests/suspense.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index e5763674d..35bf47816 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -1,6 +1,6 @@ //! Tests related to safety of the library. -use std::rc::Rc; + use dioxus::prelude::*; diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 2a64dc53c..e7c63cc98 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,8 +1,8 @@ -use dioxus::core::Mutation::*; + use dioxus::prelude::*; -use std::future::IntoFuture; -use std::rc::Rc; -use std::time::Duration; + + + #[tokio::test] async fn it_works() { @@ -28,7 +28,7 @@ fn app(cx: Scope) -> Element { } fn suspended_child(cx: Scope) -> Element { - let mut val = use_state(cx, || 0); + let val = use_state(cx, || 0); if **val < 3 { let mut val = val.clone(); From e751f63030e9be9d2b8059f0f32ffa9a5bc4011b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:17:08 -0700 Subject: [PATCH 21/23] Dont use suspense in dog app, remove from use_future --- examples/dog_app.rs | 4 ++-- packages/hooks/src/usefuture.rs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 510550b99..243e4bbca 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -21,7 +21,7 @@ fn app_root(cx: Scope<'_>) -> Element { .await }); - match breeds.suspend()? { + match breeds.value()? { Ok(breed_list) => cx.render(rsx! { div { height: "500px", h1 { "Select a dog breed!" } @@ -59,7 +59,7 @@ fn breed_pic(cx: Scope, breed: String) -> Element { .await }); - match fut.suspend()? { + match fut.value()? { Ok(resp) => render! { div { button { diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 9d39e82de..18bf50310 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -167,10 +167,6 @@ impl UseFuture { (Some(_), None) => UseFutureState::Pending, } } - - pub fn suspend(&self) -> Option<&T> { - todo!() - } } impl<'a, T> IntoFuture for &'a UseFuture { From cc984fae5402340a5f94703c13437e2d36959e5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:45:48 -0700 Subject: [PATCH 22/23] Only check suspense if the component aborted rendering --- packages/core/src/any_props.rs | 3 +-- packages/core/src/scope_arena.rs | 10 ++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index e3a3bdaef..00ccda597 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,11 +1,10 @@ -use std::{ panic::AssertUnwindSafe}; - use crate::{ innerlude::Scoped, nodes::RenderReturn, scopes::{Scope, ScopeState}, Element, }; +use std::panic::AssertUnwindSafe; /// A trait that essentially allows VComponentProps to be used generically /// diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 39b39f8c4..d058e34de 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -83,10 +83,12 @@ impl VirtualDom { id: scope.id, }); - if scope.suspended.get() { - self.suspended_scopes.insert(scope.id); - } else if !self.suspended_scopes.is_empty() { - _ = self.suspended_scopes.remove(&scope.id); + if matches!(allocated, RenderReturn::Aborted(_)) { + if scope.suspended.get() { + self.suspended_scopes.insert(scope.id); + } else if !self.suspended_scopes.is_empty() { + _ = self.suspended_scopes.remove(&scope.id); + } } // rebind the lifetime now that its stored internally From 68253b49d7eaebcf8b87bcda3e61537aded2de4e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 14 Jul 2023 17:46:14 -0700 Subject: [PATCH 23/23] cargo fmt --- packages/core/src/lib.rs | 5 ++--- packages/core/tests/safety.rs | 2 -- packages/core/tests/suspense.rs | 4 ---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 514999242..565ae12be 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -72,9 +72,8 @@ pub(crate) mod innerlude { pub use crate::innerlude::{ fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, - Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, - TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, - VirtualDom, + Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types diff --git a/packages/core/tests/safety.rs b/packages/core/tests/safety.rs index 35bf47816..7a5c0dc2a 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -1,7 +1,5 @@ //! Tests related to safety of the library. - - use dioxus::prelude::*; /// Ensure no issues with not calling rebuild diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index e7c63cc98..861b609ca 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,9 +1,5 @@ - use dioxus::prelude::*; - - - #[tokio::test] async fn it_works() { // wait just a moment, not enough time for the boundary to resolve