merge new suspense

This commit is contained in:
Evan Almloff 2023-07-17 09:56:38 -07:00
commit 913d1f0491
40 changed files with 361 additions and 656 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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<String> = || "".to_string();
const TITLE: Atom<String> = 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<String, String> = |_| {};
const DICT: AtomFamily<String, String> = 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.

View file

@ -143,7 +143,7 @@ async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
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<String> = |_| "default".to_string();
static USERNAME: Atom<String> = 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<SyncAction>, 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 {

View file

@ -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 })
}
```

View file

@ -113,14 +113,14 @@ enum InputError {
TooShort,
}
static INPUT_ERROR: Atom<InputError> = |_| InputError::None;
static INPUT_ERROR: Atom<InputError> = 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 {

View file

@ -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 })
}
```

View file

@ -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<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState<DarkMode>)`, which you can call `.read` or `.write` on, similarly to `UseRef`. Otherwise, the value is `None`.
> `use_shared_state` returns `Option<UseSharedState<DarkMode>>` here. If the context has been provided, the value is `Some(UseSharedState<DarkMode>)`, 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):

View file

@ -105,7 +105,7 @@ async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
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<String> = |_| "default".to_string();
static USERNAME: Atom<String> = 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<SyncAction>, 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 {

View file

@ -113,14 +113,14 @@ enum InputError {
TooShort,
}
static INPUT_ERROR: Atom<InputError> = |_| InputError::None;
static INPUT_ERROR: Atom<InputError> = 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 {

View file

@ -10,7 +10,7 @@ struct ListBreeds {
message: HashMap<String, Vec<String>>,
}
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 {

View file

@ -7,11 +7,11 @@ fn main() {
dioxus_desktop::launch(app)
}
static NAME: Atom<String> = |_| "world".to_string();
static NAME: Atom<String> = 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<String>> = |_| vec!["world".to_string()];
static NAMES: AtomRef<Vec<String>> = 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 {

77
examples/shared_state.rs Normal file
View file

@ -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<usize, String>,
}
impl CoolData {
pub fn new(data: HashMap<usize, String>) -> 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::<CoolData>(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::<CoolData>(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}"
})
}

View file

@ -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 = []

View file

@ -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<A>,
}
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(),
}
}
}

View file

@ -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::<Rc<SuspenseContext>>() {
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<Mutation>, Vec<Mutation>>(
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>,

View file

@ -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

View file

@ -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

View file

@ -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<Output = Element<'a>> + 'a>),
}
impl<'a> Default for RenderReturn<'a> {
@ -688,32 +684,6 @@ impl<T: Any + PartialEq + 'static> 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<Output = Element<'a>> + 'a,
{
fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> {
let f: &mut dyn Future<Output = Element<'a>> = 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) }

View file

@ -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()
}

View file

@ -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<Slab<LocalTask>>,
/// Async components
pub leaves: RefCell<Slab<SuspenseLeaf>>,
}
impl Scheduler {
@ -40,7 +32,6 @@ impl Scheduler {
Rc::new(Scheduler {
sender,
tasks: RefCell::new(Slab::new()),
leaves: RefCell::new(Slab::new()),
})
}
}

View file

@ -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<HashSet<SuspenseId>>,
pub(crate) mutations: RefCell<Mutations<'static>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
pub(crate) created_on_stack: Cell<usize>,
pub(crate) waiting_on: RefCell<HashSet<ScopeId>>,
}
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<bool>,
pub(crate) task: *mut dyn Future<Output = Element<'static>>,
pub(crate) waker: Waker,
}
pub struct SuspenseHandle {
pub(crate) id: SuspenseId,
pub(crate) tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
}
impl ArcWake for SuspenseHandle {
fn wake_by_ref(arc_self: &Arc<Self>) {
_ = arc_self
.tx
.unbounded_send(SchedulerMsg::SuspenseNotified(arc_self.id));
pub fn mark_suspend(&self, id: ScopeId) {
self.waiting_on.borrow_mut().insert(id);
}
}

View file

@ -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<SuspenseContext> {
self.scopes[id]
.consume_context::<Rc<SuspenseContext>>()
.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);
}
}
}
}
}

View file

@ -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() }
}

View file

@ -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<bool>,
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
pub(crate) hook_idx: Cell<usize>,
@ -182,7 +182,6 @@ pub struct ScopeState {
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
}
impl<'src> ScopeState {
@ -574,9 +573,9 @@ impl<'src> ScopeState {
/// fn(Scope<Props>) -> Element;
/// async fn(Scope<Props<'_>>) -> Element;
/// ```
pub fn component<P, A, F: ComponentReturn<'src, A>>(
pub fn component<P>(
&'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<Element> {
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`.

View file

@ -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<ScopeId>,
pub(crate) collected_leaves: Vec<SuspenseId>,
// 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<ScopeId>,
// Currently suspended scopes
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
@ -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::<Rc<SuspenseContext>>()
.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::<Rc<SuspenseContext>>().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::<Rc<SuspenseContext>>()
.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();

View file

@ -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::<Rc<SuspenseContext>>().is_some());
}

View file

@ -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, "<div>Waiting for... child</div>");
// 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::<Rc<SuspenseContext>>().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")
}

View file

@ -1,4 +1,3 @@
<div align="center">
<h1>Fermi ⚛</h1>
<p>
@ -6,7 +5,6 @@
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/fermi">
@ -30,21 +28,21 @@
</a>
</div>
-----
---
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<i32> = |_| 0;
static COUNT: Atom<i32> = 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

View file

@ -1,24 +1,21 @@
use crate::{AtomId, AtomRoot, Readable, Writable};
pub type Atom<T> = fn(AtomBuilder) -> T;
pub struct Atom<T>(pub fn(AtomBuilder) -> T);
pub struct AtomBuilder;
impl<V: 'static> Readable<V> for Atom<V> {
impl<V> Readable<V> for &'static Atom<V> {
fn read(&self, _root: AtomRoot) -> Option<V> {
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::<V>(),
}
*self as *const Atom<V> as *const ()
}
}
impl<V: 'static> Writable<V> for Atom<V> {
impl<V> Writable<V> for &'static Atom<V> {
fn write(&self, _root: AtomRoot, _value: V) {
todo!()
}
@ -26,6 +23,22 @@ impl<V: 'static> Writable<V> for Atom<V> {
#[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<Vec<S>> = Atom(|_| Vec::new());
static TEST_ATOM_2: Atom<Vec<String>> = Atom(|_| Vec::new());
assert_ne!((&TEST_ATOM_1).unique_id(), (&TEST_ATOM_2).unique_id());
}

View file

@ -2,26 +2,23 @@ use crate::{AtomId, AtomRoot, Readable, Writable};
use im_rc::HashMap as ImMap;
pub struct AtomFamilyBuilder;
pub type AtomFamily<K, V> = fn(AtomFamilyBuilder) -> ImMap<K, V>;
pub struct AtomFamily<K, V>(pub fn(AtomFamilyBuilder) -> ImMap<K, V>);
impl<K, V: 'static> Readable<ImMap<K, V>> for AtomFamily<K, V> {
impl<K, V> Readable<ImMap<K, V>> for &'static AtomFamily<K, V> {
fn read(&self, _root: AtomRoot) -> Option<ImMap<K, V>> {
todo!()
}
fn init(&self) -> ImMap<K, V> {
(*self)(AtomFamilyBuilder)
self.0(AtomFamilyBuilder)
}
fn unique_id(&self) -> AtomId {
AtomId {
ptr: *self as *const (),
type_id: std::any::TypeId::of::<V>(),
}
*self as *const AtomFamily<K, V> as *const ()
}
}
impl<K, V: 'static> Writable<ImMap<K, V>> for AtomFamily<K, V> {
impl<K, V> Writable<ImMap<K, V>> for &'static AtomFamily<K, V> {
fn write(&self, _root: AtomRoot, _value: ImMap<K, V>) {
todo!()
}

View file

@ -2,27 +2,24 @@ use crate::{AtomId, AtomRoot, Readable};
use std::cell::RefCell;
pub struct AtomRefBuilder;
pub type AtomRef<T> = fn(AtomRefBuilder) -> T;
pub struct AtomRef<T>(pub fn(AtomRefBuilder) -> T);
impl<V: 'static> Readable<RefCell<V>> for AtomRef<V> {
impl<V> Readable<RefCell<V>> for &'static AtomRef<V> {
fn read(&self, _root: AtomRoot) -> Option<RefCell<V>> {
todo!()
}
fn init(&self) -> RefCell<V> {
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::<V>(),
}
*self as *const AtomRef<V> as *const ()
}
}
#[test]
fn atom_compiles() {
static TEST_ATOM: AtomRef<Vec<String>> = |_| vec![];
dbg!(TEST_ATOM.init());
static TEST_ATOM: AtomRef<Vec<String>> = AtomRef(|_| vec![]);
dbg!((&TEST_ATOM).init());
}

View file

@ -13,7 +13,10 @@ use std::{
///
///
///
pub fn use_atom_ref<T: 'static>(cx: &ScopeState, atom: AtomRef<T>) -> &UseAtomRef<T> {
pub fn use_atom_ref<'a, T: 'static>(
cx: &'a ScopeState,
atom: &'static AtomRef<T>,
) -> &'a UseAtomRef<T> {
let root = use_atom_root(cx);
&cx.use_hook(|| {

View file

@ -19,7 +19,7 @@ use std::{
/// static COUNT: Atom<u32> = |_| 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 {

View file

@ -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<HashMap<AtomId, Slot>>,
@ -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::<V>()
),
}
slot.value.clone().downcast().unwrap()
} else {
let value = Rc::new(f.init());
let mut subscribers = HashSet::new();

View file

@ -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<T, F, D>(cx: &ScopeState, dependencies: D, future: impl FnOnce(D::Out) -> F)

View file

@ -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<T, D>(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T