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/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 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/Cargo.toml b/Cargo.toml index 880e9b129..30e190300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,8 +120,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 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/__unused/advanced-guides/12-signals.md b/docs/guide/src/en/__unused/advanced-guides/12-signals.md index df13b91d8..7fbb56de7 100644 --- a/docs/guide/src/en/__unused/advanced-guides/12-signals.md +++ b/docs/guide/src/en/__unused/advanced-guides/12-signals.md @@ -94,8 +94,10 @@ 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, no_run -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 +133,8 @@ 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, no_run -const DICT: AtomFamily = |_| {}; +const DICT: AtomFamily = AtomFamily(|_| {}); + const List: Component = |cx|{ let dict = use_signal(cx, &DICT); cx.render(rsx!( @@ -142,14 +145,6 @@ const List: Component = |cx|{ }; ``` -## Remote Signals - -Apps that use signals will enjoy a pleasant hybrid of server-side and client-side rendering. - -```rust, no_run - -``` - ## How does it work? Signals internally use Dioxus' asynchronous rendering infrastructure to perform updates out of the tree. diff --git a/docs/guide/src/en/async/use_coroutine.md b/docs/guide/src/en/async/use_coroutine.md index 8d4b8f99a..d42905be5 100644 --- a/docs/guide/src/en/async/use_coroutine.md +++ b/docs/guide/src/en/async/use_coroutine.md @@ -143,7 +143,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, no_run -static USERNAME: Atom = |_| "default".to_string(); +static USERNAME: Atom = Atom(|_| "default".to_string()); fn app(cx: Scope) -> Element { let atoms = use_atom_root(cx); @@ -156,7 +156,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}" } @@ -174,8 +174,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/async/use_effect.md b/docs/guide/src/en/async/use_effect.md new file mode 100644 index 000000000..7c20e3589 --- /dev/null +++ b/docs/guide/src/en/async/use_effect.md @@ -0,0 +1,41 @@ +# 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 its [dependencies](#dependencies) change. This is useful to syncrhonize with external events. + +## Dependencies + +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 + +```rust, no_run +#[inline_props] +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,)| { + to_owned![name]; + async move { + let user = fetch_user(id).await; + name.set(user.name); + } + }); + + // 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!( + p { "{name}" } + ) +} + +fn app(cx: Scope) -> Element { + render!(Profile { id: 0 }) +} +``` diff --git a/docs/guide/src/en/best_practices/error_handling.md b/docs/guide/src/en/best_practices/error_handling.md index fad8f1c10..d16d04347 100644 --- a/docs/guide/src/en/best_practices/error_handling.md +++ b/docs/guide/src/en/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); ``` Then, in our top level component, we want to explicitly handle the possible error state for this part of the tree. ```rust, no_run 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 @@ Now, whenever a downstream component has an error in its actions, it can simply ```rust, no_run 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/en/interactivity/memoization.md b/docs/guide/src/en/interactivity/memoization.md new file mode 100644 index 000000000..32ec37ce5 --- /dev/null +++ b/docs/guide/src/en/interactivity/memoization.md @@ -0,0 +1,19 @@ +# Memoization + +[`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] +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 4c97e53bf..d71fbdec0 100644 --- a/docs/guide/src/en/interactivity/sharing_state.md +++ b/docs/guide/src/en/interactivity/sharing_state.md @@ -32,7 +32,7 @@ Finally, a third component will render the other two as children. It will be res ![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. @@ -42,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: @@ -62,7 +62,7 @@ As a result, any child component of `App` (direct or not), can access the `DarkM {{#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): diff --git a/docs/guide/src/pt-br/async/use_coroutine.md b/docs/guide/src/pt-br/async/use_coroutine.md index d7edbd8cc..cb9eda3a0 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, no_run -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 7e027e9d0..2fec8d88d 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, no_run 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, no_run 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/dog_app.rs b/examples/dog_app.rs index 107251cd7..243e4bbca 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.value()? { + 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.value()? { Ok(resp) => render! { div { button { 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/examples/shared_state.rs b/examples/shared_state.rs new file mode 100644 index 000000000..d129f75d1 --- /dev/null +++ b/examples/shared_state.rs @@ -0,0 +1,77 @@ +#![allow(non_snake_case)] + +use std::collections::HashMap; + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(App); +} + +#[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, || CoolData::new(HashMap::from([ + (0, "Hello, World!".to_string()), + (1, "Dioxus is amazing!".to_string()) + ]))); + + render!( + 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}" + }) +} 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/any_props.rs b/packages/core/src/any_props.rs index 7014a2771..00ccda597 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,11 +1,10 @@ -use std::{marker::PhantomData, panic::AssertUnwindSafe}; - use crate::{ innerlude::Scoped, - nodes::{ComponentReturn, RenderReturn}, + nodes::RenderReturn, scopes::{Scope, ScopeState}, Element, }; +use std::panic::AssertUnwindSafe; /// A trait that essentially allows VComponentProps to be used generically /// @@ -18,19 +17,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 +33,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 +60,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..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,9 +510,9 @@ 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), - Pending(_) => self.mount_async(template, idx, scope), } } @@ -530,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 }); @@ -591,24 +535,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..cfda18505 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)); @@ -139,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 @@ -160,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."), @@ -190,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; @@ -198,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 @@ -235,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(); @@ -297,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)), } } @@ -735,7 +715,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 +916,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/lib.rs b/packages/core/src/lib.rs index 304197dab..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, SuspenseContext, - 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/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..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. @@ -18,9 +16,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 +25,6 @@ pub(crate) struct Scheduler { /// Tasks created with cx.spawn pub tasks: RefCell>, - - /// Async components - pub leaves: RefCell>, } impl Scheduler { @@ -40,7 +32,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..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,29 +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(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)); + 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 77bea3a18..0a7dce207 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::{TaskId, VirtualDom}; +use std::task::Context; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -38,74 +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() - } - - 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..d058e34de 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( @@ -33,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(), @@ -58,74 +50,21 @@ 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]; + scope.suspended.set(false); scope.hook_idx.set(0); // 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 @@ -144,6 +83,14 @@ impl VirtualDom { id: 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 unsafe { allocated.extend_lifetime_ref() } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index ff94c456d..8d980222c 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,12 +1,11 @@ use crate::{ any_props::AnyProps, any_props::VProps, - arena::ElementId, bump_frame::BumpFrame, 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}; @@ -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 { @@ -574,9 +573,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> @@ -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 b118cd59a..31a1b5912 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,25 +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 { - !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 @@ -485,7 +456,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 @@ -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; } @@ -513,7 +483,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 +543,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() @@ -598,6 +566,21 @@ 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() { + return; + } + + self.wait_for_work().await; + + _ = self.render_immediate(); + } + } + /// 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. @@ -609,26 +592,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() { @@ -639,40 +602,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 @@ -680,11 +612,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/safety.rs b/packages/core/tests/safety.rs index 8c125f4c6..7a5c0dc2a 100644 --- a/packages/core/tests/safety.rs +++ b/packages/core/tests/safety.rs @@ -1,9 +1,6 @@ //! Tests related to safety of the library. -use std::rc::Rc; - use dioxus::prelude::*; -use dioxus_core::SuspenseContext; /// Ensure no issues with not calling rebuild #[test] @@ -17,8 +14,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 06bb7b1a4..861b609ca 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,100 +1,38 @@ -use dioxus::core::ElementId; -use dioxus::core::{Mutation::*, SuspenseContext}; 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 { - cx.use_hook(|| { - cx.provide_context(Rc::new(SuspenseContext::new(cx.scope_id()))); - }); +fn suspended_child(cx: Scope) -> Element { + let 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 {})) -} - -async fn async_child(cx: Scope<'_>) -> Element { - 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" - }); - - 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") } diff --git a/packages/fermi/README.md b/packages/fermi/README.md index f276dbff5..ff6635b0f 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 81e04ce0b..bba1f532f 100644 --- a/packages/fermi/src/atoms/atom.rs +++ b/packages/fermi/src/atoms/atom.rs @@ -1,24 +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 { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *self as *const Atom as *const () } } -impl Writable for Atom { +impl Writable for &'static Atom { fn write(&self, _root: AtomRoot, _value: V) { todo!() } @@ -26,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 29cecd4b2..45f6b8feb 100644 --- a/packages/fermi/src/atoms/atomfamily.rs +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -2,26 +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 { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *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 c103fa662..5e863ade2 100644 --- a/packages/fermi/src/atoms/atomref.rs +++ b/packages/fermi/src/atoms/atomref.rs @@ -2,27 +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 { - AtomId { - ptr: *self as *const (), - type_id: std::any::TypeId::of::(), - } + *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 { 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(); diff --git a/packages/hooks/src/useeffect.rs b/packages/hooks/src/useeffect.rs index f9ef70b84..aec1f9c9a 100644 --- a/packages/hooks/src/useeffect.rs +++ b/packages/hooks/src/useeffect.rs @@ -9,17 +9,33 @@ 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: usize) -> Element { +/// let name = use_state(cx, || None); +/// +/// // 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: 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 8cc5540e6..d3d991ccc 100644 --- a/packages/hooks/src/usememo.rs +++ b/packages/hooks/src/usememo.rs @@ -2,21 +2,27 @@ 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