Merge branch 'breaking' of https://github.com/Demonthos/dioxus into breaking

This commit is contained in:
Evan Almloff 2024-01-30 11:10:47 -06:00
commit 11555e4a51
22 changed files with 551 additions and 523 deletions

View file

@ -5,23 +5,19 @@ fn main() {
}
fn app() -> Element {
let mut state = use_signal(|| 0);
let mut value = use_signal(|| 0);
let mut depth = use_signal(|| 0 as usize);
let mut items = use_memo(move || (0..depth()).map(|f| f as _).collect::<Vec<isize>>());
let a = use_memo(move || state() + 1);
let state = use_memo(move || value() + 1);
println!("rendering app");
rsx! {
button { onclick: move |_| state += 1, "Increment" }
button { onclick: move |_| value += 1, "Increment" }
button { onclick: move |_| depth += 1, "Add depth" }
button { onclick: move |_| depth -= 1, "Remove depth" }
Child {
depth: depth.into(),
items: items,
state: a,
}
Child { depth, items, state }
}
}
@ -36,7 +32,6 @@ fn Child(
}
// These memos don't get re-computed when early returns happen
// In dioxus futures spawned with use_future won't progress if they don't get hit during rendering
let state = use_memo(move || state() + 1);
let item = use_memo(move || items()[depth()]);
let depth = use_memo(move || depth() - 1);
@ -45,10 +40,6 @@ fn Child(
rsx! {
h3 { "Depth({depth})-Item({item}): {state}"}
Child {
depth,
state,
items
}
Child { depth, state, items }
}
}

View file

@ -17,6 +17,17 @@ fn app() -> Element {
// effects will always run after first mount and then whenever the signal values change
use_effect(move || println!("Count changed to {}", count()));
// We can do early returns and conditional rendering which will pause all futures that haven't been polled
if count() > 30 {
return rsx! {
h1 { "Count is too high!" }
button {
onclick: move |_| count.set(0),
"Press to reset"
}
};
}
// use_future will spawn an infinitely running future that can be started and stopped
use_future(|| async move {
loop {

View file

@ -1,35 +1,54 @@
# dioxus-core
dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust.
`dioxus-core` provides a fast and featureful VirtualDom implementation for Rust.
# Features
```rust, ignore
use dioxus_core::prelude::*;
- Functions as components
- Hooks for local state
- Task pool for spawning futures
- Template-based architecture
- Asynchronous components
- Suspense boundaries
- Error boundaries through the `anyhow` crate
- Customizable memoization
let vdom = VirtualDom::new(app);
let real_dom = SomeRenderer::new();
loop {
select! {
evt = real_dom.event() => vdom.handle_event(evt),
_ = vdom.wait_for_work() => {}
}
vdom.render(&mut real_dom)
}
# fn app() -> Element { None }
# struct SomeRenderer; impl SomeRenderer { fn new() -> SomeRenderer { SomeRenderer; } async fn event() -> () { todo!() } }
```
## Features
A virtualdom is an efficient and flexible tree datastructure that allows you to manage state for a graphical user interface. The Dioxus VirtualDom is perhaps the most fully-featured virtualdom implementation in Rust and powers renderers running across Web, Desktop, Mobile, SSR, TUI, LiveView, and more. When you use the Dioxus VirtualDom, you immediately enable users of your renderer to leverage the wide ecosystem of Dioxus components, hooks, and associated tooling.
Some features of `dioxus-core` include:
- UI components are just functions
- State is provided by hooks
- Deep integration with async
- Strong focus on performance
- Integrated hotreloading support
- Extensible system for UI elements and their attributes
If you are just starting, check out the Guides first.
# General Theory
## Understanding the implementation
The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime.
`dioxus-core` is designed to be a lightweight crate that. It exposes a number of flexible primitives without being deeply concerned about the intracices of state management itself. We proivde a number of useful abstractions built on these primitives in the `dioxus-hooks` crate as well as the `dioxus-signals` crate.
Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves.
The important abstractions to understand are:
- The [`VirtualDom`]
- The [`Component`] and its [`Properties`]
- Handling events
- Working with async
- Suspense
When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result<VNode>`. Between two renders, Dioxus compares the inner `VNode` object and calculates the differences between the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and the new layout, Dioxus will write modifications to the `Mutations` object.
## Usage
Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups.
# Usage
All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`].
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates.
The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust.
First, start with your app:
@ -53,67 +72,22 @@ fn main() {
}
```
## Contributing
- Check out the website [section on contributing](https://dioxuslabs.com/learn/0.4/contributing).
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- [Join](https://discord.gg/XgGxMSkvUM) the discord and ask questions!
We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead:
```rust
# #![allow(unused)]
# use dioxus::prelude::*;
# use std::time::Duration;
# async fn wait(mut dom: VirtualDom) {
// Wait for the dom to be marked dirty internally
dom.wait_for_work().await;
# }
```
<a href="https://github.com/dioxuslabs/dioxus/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dioxuslabs/dioxus&max=30&columns=10" />
</a>
If an event occurs from outside the VirtualDom while waiting for work, then we can cancel the wait using a `select!` block and inject the event.
```rust, ignore
loop {
select! {
evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles),
_ = dom.wait_for_work() => {}
}
## License
This project is licensed under the [MIT license].
// Render any work without blocking the main thread for too long
let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)));
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
// And then apply the edits
real_dom.apply(mutations);
}
```
## Internals
Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus borrows these concepts:
- React: hooks, concurrency, suspense
- Dodrio: bump allocation, double buffering, and some diffing architecture
Dioxus-core hits a very high level of parity with mature frameworks. However, Dioxus also brings some new unique features:
- managed lifetimes for borrowed data
- placeholder approach for suspended vnodes
- fiber/interruptible diffing algorithm
- custom memory allocator for VNodes and all text content
- support for fragments w/ lazy normalization
- slab allocator for scopes
- mirrored-slab approach for remote VirtualDoms
- dedicated subtrees for rendering into separate contexts from the same app
There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, no allocations may be needed once the app has been loaded. Only when new components are added to the dom will allocations occur. For a given component, the space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components.
All in all, Dioxus treats memory as a valuable resource. Combined with the memory-efficient footprint of Wasm compilation, Dioxus apps can scale to thousands of components and still stay snappy.
## Goals
The final implementation of Dioxus must:
- Be **fast**. Allocators are typically slow in Wasm/Rust, so we should have a smart way of allocating.
- Be memory efficient. Servers should handle tens of thousands of simultaneous VDoms with no problem.
- Be concurrent. Components should be able to pause rendering to let the screen paint the next frame.
- Be disconnected from a specific renderer (no WebSys dependency in the core crate).
- Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server.
- Be "live". Components should be able to be both server-rendered and client rendered without needing frontend APIs.
- Be modular. Components and hooks should work anywhere without worrying about the target platform.
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you, shall be licensed as MIT, without any additional
terms or conditions.

View file

@ -216,17 +216,33 @@ pub fn use_drop<D: FnOnce() + 'static>(destroy: D) {
});
}
pub fn use_before_render(f: impl FnMut() + 'static) {
use_hook(|| before_render(f));
}
pub fn use_after_render(f: impl FnMut() + 'static) {
use_hook(|| after_render(f));
}
/// Push a function to be run before the next render
/// This is a hook and will always run, so you can't unschedule it
/// Will run for every progression of suspense, though this might change in the future
pub fn use_before_render(f: impl FnMut() + 'static) {
pub fn before_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_before_render(f));
}
/// Push a function to be run after the render is complete, even if it didn't complete successfully
pub fn after_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_after_render(f));
}
/// Wait for the virtualdom to finish its sync work before proceeding
///
/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid
/// DOM nodes.
///
/// Effects rely on this to ensure that they only run effects after the DOM has been updated. Without flush_sync effects
/// are run immediately before diffing the DOM, which causes all sorts of out-of-sync weirdness.
pub async fn flush_sync() {
let mut polled = false;
@ -246,7 +262,8 @@ pub async fn flush_sync() {
.await;
// If the the future got polled, then we don't need to prevent it from being dropped
// This would all be solved with generational indicies on tasks
// If we had generational indicies on tasks we could simply let the task remain in the queue and just be a no-op
// when it's run
std::mem::forget(_task);
struct FlushKey(Task);

View file

@ -90,9 +90,9 @@ pub mod prelude {
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, flush_sync,
generation, has_context, needs_update, parent_scope, provide_context, provide_root_context,
remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, suspend,
try_consume_context, use_before_render, use_drop, use_error_boundary, use_hook,
use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction, Element,
ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary,
use_hook, use_hook_with_cleanup, AnyValue, Attribute, Component, ComponentFunction,
Element, ErrorBoundary, Event, EventHandler, Fragment, HasAttributes, IntoAttributeValue,
IntoDynNode, OptionStringFromMarker, Properties, Runtime, RuntimeGuard, ScopeId,
ScopeState, SuperFrom, SuperInto, Task, Template, TemplateAttribute, TemplateNode, Throw,
VNode, VNodeInner, VirtualDom,

View file

@ -57,6 +57,11 @@ impl VirtualDom {
let context = scope.state();
// Run all post-render hooks
for post_run in context.after_render.borrow_mut().iter_mut() {
post_run();
}
// And move the render generation forward by one
context.render_count.set(context.render_count.get() + 1);

View file

@ -24,6 +24,7 @@ pub(crate) struct Scope {
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
pub(crate) after_render: RefCell<Vec<Box<dyn FnMut()>>>,
}
impl Scope {
@ -45,6 +46,7 @@ impl Scope {
hooks: RefCell::new(vec![]),
hook_index: Cell::new(0),
before_render: RefCell::new(vec![]),
after_render: RefCell::new(vec![]),
}
}
@ -285,6 +287,10 @@ impl Scope {
self.before_render.borrow_mut().push(Box::new(f));
}
pub fn push_after_render(&self, f: impl FnMut() + 'static) {
self.after_render.borrow_mut().push(Box::new(f));
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc

View file

@ -58,6 +58,9 @@ impl Task {
Runtime::with(|rt| {
// set the active flag, and then ping the scheduler to ensure the task gets queued
let was_active = rt.tasks.borrow()[self.0].active.replace(true);
if !was_active {
_ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self));
}
});
}
}

View file

@ -422,8 +422,15 @@ impl VirtualDom {
// Ping tasks waiting on the flush table - they're waiting for sync stuff to be done before progressing
self.clear_flush_table();
// And then poll the futures
self.poll_tasks().await;
}
/// Poll futures without progressing any futures from the flush table
async fn poll_tasks(&mut self) {
loop {
// Process all events - Scopes are marked dirty, etc
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
self.process_events();
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
@ -542,6 +549,7 @@ impl VirtualDom {
self.flush_templates(to);
// Process any events that might be pending in the queue
// Signals marked with .write() need a chance to be handled by the effect driver
self.process_events();
// Next, diff any dirty scopes
@ -573,19 +581,18 @@ impl VirtualDom {
///
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
///
/// Tasks waiting to be flushed are *cleared* here *without running them*
/// This behavior is subject to change, but configured this way so use_future/use_memo/use_future won't react on the server
/// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
/// however any futures wating on flush_sync will remain pending
pub async fn wait_for_suspense(&mut self) {
loop {
if self.suspended_scopes.is_empty() {
return;
break;
}
// not sure if we should be doing this?
self.runtime.flush_table.borrow_mut().clear();
self.wait_for_work().await;
// Wait for a work to be ready (IE new suspense leaves to pop up)
self.poll_tasks().await;
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
self.render_immediate(&mut NoOpMutations);
}
}

View file

@ -54,6 +54,8 @@ macro_rules! to_owned {
$(to_owned![$($rest)*])?
};
}
mod dependency;
pub use dependency::*;
mod use_callback;
pub use use_callback::*;
@ -76,8 +78,20 @@ pub use use_sorted::*;
mod use_resource;
pub use use_resource::*;
mod use_effect;
pub use use_effect::*;
mod use_memo;
pub use use_memo::*;
// mod use_on_create;
// pub use use_on_create::*;
mod use_root_context;
pub use use_root_context::*;
mod use_hook_did_run;
pub use use_hook_did_run::*;
mod use_signal;
pub use use_signal::*;

View file

@ -0,0 +1,24 @@
use crate::use_hook_did_run;
use dioxus_core::prelude::*;
use dioxus_signals::{CopyValue, Effect, Writable};
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
/// The signal will be owned by the current component and will be dropped when the component is dropped.
///
/// If the use_effect call was skipped due to an early return, the effect will no longer activate.
pub fn use_effect(mut callback: impl FnMut() + 'static) {
let mut run_effect = use_hook(|| CopyValue::new(true));
use_hook_did_run(move |did_run| match did_run {
true => run_effect.set(true),
false => run_effect.set(false),
});
use_hook(|| {
Effect::new(move || {
if run_effect() {
callback();
}
})
});
}

View file

@ -1,14 +1,12 @@
#![allow(missing_docs)]
use crate::{use_hook_did_run, use_signal};
use dioxus_core::{
prelude::{spawn, use_before_render, use_drop, use_hook},
ScopeState, Task,
prelude::{spawn, use_drop, use_hook},
Task,
};
use dioxus_signals::*;
use dioxus_signals::{Readable, Writable};
use futures_util::{future, pin_mut, FutureExt};
use std::{any::Any, cell::Cell, future::Future, pin::Pin, rc::Rc, sync::Arc, task::Poll};
use crate::use_callback;
use std::future::Future;
/// A hook that allows you to spawn a future
///
@ -17,44 +15,28 @@ pub fn use_future<F>(mut future: impl FnMut() -> F) -> UseFuture
where
F: Future + 'static,
{
let state = use_signal(|| UseFutureState::Pending);
let mut state = use_signal(|| UseFutureState::Pending);
// Create the task inside a copyvalue so we can reset it in-place later
let task = use_hook(|| {
let fut = future();
CopyValue::new(spawn(async move {
fut.await;
state.set(UseFutureState::Complete);
}))
});
/*
Early returns in dioxus have consequences for use_memo, use_resource, and use_future, etc
We *don't* want futures to be running if the component early returns. It's a rather weird behavior to have
use_memo running in the background even if the component isn't hitting those hooks anymore.
React solves this by simply not having early returns interleave with hooks.
However, since dioxus allows early returns (since we use them for suspense), we need to solve this problem.
*/
// Track if this *current* render is the same
let gen = use_hook(|| CopyValue::new((0, 0)));
// Early returns will pause this task, effectively
use_before_render(move || {
gen.write().0 += 1;
task.peek().set_active(false);
// Early returns in dioxus have consequences for use_memo, use_resource, and use_future, etc
// We *don't* want futures to be running if the component early returns. It's a rather weird behavior to have
// use_memo running in the background even if the component isn't hitting those hooks anymore.
//
// React solves this by simply not having early returns interleave with hooks.
// However, since dioxus allows early returns (since we use them for suspense), we need to solve this problem
use_hook_did_run(move |did_run| match did_run {
true => task.peek().resume(),
false => task.peek().pause(),
});
// However when we actually run this component, we want to resume the task
task.peek().set_active(true);
gen.write().1 += 1;
// if the gens are different, we need to wake the task
if gen().0 != gen().1 {
task.peek().wake();
}
use_drop(move || task.peek().stop());
UseFuture { task, state }

View file

@ -0,0 +1,16 @@
use dioxus_core::prelude::*;
use dioxus_signals::{CopyValue, Writable};
/// A hook that uses before/after lifecycle hooks to determine if the hook was run
pub fn use_hook_did_run(mut handler: impl FnMut(bool) + 'static) {
let mut did_run_ = use_hook(|| CopyValue::new(false));
// Before render always set the value to false
use_before_render(move || did_run_.set(false));
// Only when this hook is hit do we want to set the value to true
did_run_.set(true);
// After render, we can check if the hook was run
use_after_render(move || handler(did_run_()));
}

View file

@ -1,47 +1,123 @@
use dioxus_core::ScopeState;
use dioxus_core::prelude::*;
use dioxus_signals::{CopyValue, ReadOnlySignal, Readable, Signal, SignalData};
use dioxus_signals::{Storage, Writable};
// use generational_box::Storage;
use crate::UseFutureDep;
use crate::use_signal;
use crate::{dependency::Dependency, use_hook_did_run};
// use dioxus_signals::{signal::SignalData, ReadOnlySignal, Signal};
/// 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.
/// Creates a new unsync Selector. The selector will be run immediately and whenever any signal it reads changes.
///
/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ## Examples
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
///
/// #[component]
/// fn Calculator(number: usize) -> Element {
/// let bigger_number = use_memo((number,), |(number,)| {
/// // This will only be calculated when `number` has changed.
/// number * 100
/// });
/// rsx!(
/// p { "{bigger_number}" }
/// )
/// }
///
/// #[component]
/// fn App() -> Element {
/// rsx!(Calculator { number: 0 })
/// let mut count = use_signal(|| 0);
/// let double = use_memo(move || count * 2);
/// count += 1;
/// assert_eq!(double.value(), count * 2);
///
/// rsx! { "{double}" }
/// }
/// ```
#[must_use = "Consider using `use_effect` to run rerun a callback when dependencies change"]
pub fn use_memo<T, D>(, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T
where
T: 'static,
D: UseFutureDep,
{
let value = cx.use_hook(|| None);
let dependancies_vec = cx.use_hook(Vec::new);
if dependencies.clone().apply(dependancies_vec) || value.is_none() {
// Create the new value
*value = Some(callback(dependencies.out()));
}
value.as_ref().unwrap()
#[track_caller]
pub fn use_memo<R: PartialEq>(f: impl FnMut() -> R + 'static) -> ReadOnlySignal<R> {
use_maybe_sync_memo(f)
}
/// Creates a new Selector that may be sync. The selector will be run immediately and whenever any signal it reads changes.
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut count = use_signal(cx, || 0);
/// let double = use_memo(cx, move || count * 2);
/// count += 1;
/// assert_eq!(double.value(), count * 2);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_maybe_sync_memo<R: PartialEq, S: Storage<SignalData<R>>>(
f: impl FnMut() -> R + 'static,
) -> ReadOnlySignal<R, S> {
use_hook(|| Signal::maybe_sync_memo(f))
}
/// Creates a new unsync Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut local_state = use_state(cx, || 0);
/// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
/// local_state.set(1);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_memo_with_dependencies<R: PartialEq, D: Dependency>(
dependencies: D,
f: impl FnMut(D::Out) -> R + 'static,
) -> ReadOnlySignal<R>
where
D::Out: 'static,
{
use_maybe_sync_selector_with_dependencies(dependencies, f)
}
/// Creates a new Selector that may be sync with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut local_state = use_state(cx, || 0);
/// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
/// local_state.set(1);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_maybe_sync_selector_with_dependencies<
R: PartialEq,
D: Dependency,
S: Storage<SignalData<R>>,
>(
dependencies: D,
mut f: impl FnMut(D::Out) -> R + 'static,
) -> ReadOnlySignal<R, S>
where
D::Out: 'static,
{
let mut dependencies_signal = use_signal(|| dependencies.out());
let selector = use_hook(|| {
Signal::maybe_sync_memo(move || {
let deref = &*dependencies_signal.read();
f(deref.clone())
})
});
let changed = { dependencies.changed(&*dependencies_signal.read()) };
if changed {
dependencies_signal.set(dependencies.out());
}
selector
}

View file

@ -1,4 +1,5 @@
#![allow(missing_docs)]
use crate::use_signal;
use dioxus_core::{
prelude::{spawn, use_hook},
ScopeState, Task,

View file

@ -0,0 +1,102 @@
use dioxus_core::prelude::*;
use dioxus_signals::{Signal, SignalData, Storage, SyncStorage, UnsyncStorage};
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App() -> Element {
/// let mut count = use_signal(|| 0);
///
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
/// // The app component will never be rerendered in this example.
/// rsx! { Child { state: count } }
/// }
///
/// #[component]
/// fn Child(state: Signal<u32>) -> Element {
/// let state = *state;
///
/// use_future( |()| async move {
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
/// *state.write() += 1;
/// });
///
/// rsx! {
/// button {
/// onclick: move |_| *state.write() += 1,
/// "{state}"
/// }
/// }
/// }
/// ```
#[track_caller]
#[must_use]
pub fn use_signal<T: 'static>(f: impl FnOnce() -> T) -> Signal<T, UnsyncStorage> {
use_maybe_signal_sync(f)
}
/// Creates a new `Send + Sync`` Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut count = use_signal_sync(cx, || 0);
///
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
/// // The app component will never be rerendered in this example.
/// render! { Child { state: count } }
/// }
///
/// #[component]
/// fn Child(cx: Scope, state: Signal<u32, SyncStorage>) -> Element {
/// let state = *state;
///
/// use_future!(cx, |()| async move {
/// // This signal is Send + Sync, so we can use it in an another thread
/// tokio::spawn(async move {
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
/// *state.write() += 1;
/// }).await;
/// });
///
/// render! {
/// button {
/// onclick: move |_| *state.write() += 1,
/// "{state}"
/// }
/// }
/// }
/// ```
#[must_use]
#[track_caller]
pub fn use_signal_sync<T: Send + Sync + 'static>(f: impl FnOnce() -> T) -> Signal<T, SyncStorage> {
use_maybe_signal_sync(f)
}
#[must_use]
#[track_caller]
fn use_maybe_signal_sync<T: 'static, U: Storage<SignalData<T>>>(
f: impl FnOnce() -> T,
) -> Signal<T, U> {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
let signal = use_hook(|| {
Signal::new_with_caller(
f(),
#[cfg(debug_assertions)]
caller,
)
});
// By default, we want to unsubscribe the current component from the signal on every render
// any calls to .read() in the body will re-subscribe the component to the signal
use_before_render(move || signal.unsubscribe(current_scope_id().unwrap()));
signal
}

View file

@ -1,7 +1,8 @@
use std::cmp::Ordering;
use std::ops::DerefMut;
use dioxus_signals::{use_memo, ReadOnlySignal, Signal};
use crate::use_memo;
use dioxus_signals::{ReadOnlySignal, Signal};
pub fn use_sorted<V: 'static, T: PartialEq>(
collection: impl FnMut() -> Signal<V>,

View file

@ -47,60 +47,75 @@ impl EffectStackRef {
}
pub(crate) fn get_effect_ref() -> EffectStackRef {
match try_consume_context() {
Some(rt) => rt,
None => {
let (sender, mut receiver) = futures_channel::mpsc::unbounded();
spawn_forever(async move {
let mut queued_memos = Vec::new();
loop {
// Wait for a flush
// This gives a chance for effects to be updated in place and memos to compute their values
let flush_await = flush_sync();
pin_mut!(flush_await);
loop {
let res =
futures_util::future::select(&mut flush_await, receiver.next()).await;
match res {
Either::Right((_queued, _)) => {
if let Some(task) = _queued {
queued_memos.push(task);
}
continue;
}
Either::Left(_flushed) => break,
}
}
EFFECT_STACK.with(|stack| {
for id in queued_memos.drain(..) {
let effect_mapping = stack.effect_mapping.read();
if let Some(mut effect) = effect_mapping.get(&id).copied() {
tracing::trace!("Rerunning effect: {:?}", id);
effect.try_run();
} else {
tracing::trace!("Effect not found: {:?}", id);
}
}
});
}
});
let stack_ref = EffectStackRef {
rerun_effect: sender,
};
provide_root_context(stack_ref.clone());
stack_ref
}
if let Some(rt) = try_consume_context() {
return rt;
}
let (sender, receiver) = futures_channel::mpsc::unbounded();
spawn_forever(async move { effect_driver(receiver).await });
let stack_ref = EffectStackRef {
rerun_effect: sender,
};
provide_root_context(stack_ref.clone());
stack_ref
}
/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
/// The signal will be owned by the current component and will be dropped when the component is dropped.
pub fn use_effect(callback: impl FnMut() + 'static) {
use_hook(|| Effect::new(callback));
/// The primary top-level driver of all effects
///
/// In Dioxus, effects are neither react effects nor solidjs effects. They are a hybrid of the two, making our model
/// more complex but also more powerful.
///
/// In react, when a component renders, it can queue up effects to be run after the component is done rendering.
/// This is done *only during render* and determined by the dependency array attached to the effect. In Dioxus,
/// we track effects using signals, so these effects can actually run multiple times after the component has rendered.
///
///
async fn effect_driver(
mut receiver: futures_channel::mpsc::UnboundedReceiver<GenerationalBoxId>,
) -> ! {
let mut queued_memos = Vec::new();
loop {
// Wait for a flush
// This gives a chance for effects to be updated in place and memos to compute their values
let flush_await = flush_sync();
pin_mut!(flush_await);
// Until the flush is ready, wait for a new effect to be queued
// We don't run the effects immediately because we want to batch them on the next call to flush
// todo: the queued memos should be unqueued when components are dropped
loop {
match futures_util::future::select(&mut flush_await, receiver.next()).await {
// VDOM is flushed and we can run the queued effects
Either::Left(_flushed) => break,
// A new effect was queued to be run after the next flush
// Marking components as dirty is handled syncrhonously on write, though we could try
// batching them here too
Either::Right((_queued, _)) => {
if let Some(task) = _queued {
queued_memos.push(task);
}
}
}
}
EFFECT_STACK.with(|stack| {
for id in queued_memos.drain(..) {
let effect_mapping = stack.effect_mapping.read();
if let Some(mut effect) = effect_mapping.get(&id).copied() {
tracing::trace!("Rerunning effect: {:?}", id);
effect.try_run();
} else {
tracing::trace!("Effect not found: {:?}", id);
}
}
});
}
}
/// Effects allow you to run code when a signal changes. Effects are run immediately and whenever any signal it reads changes.

View file

@ -10,18 +10,12 @@ pub use rt::*;
mod effect;
pub use effect::*;
mod memo;
pub use memo::*;
pub(crate) mod signal;
pub use signal::*;
mod read_only_signal;
pub use read_only_signal::*;
mod dependency;
pub use dependency::*;
mod map;
pub use map::*;

View file

@ -1,123 +0,0 @@
use crate::read::Readable;
use crate::write::Writable;
use dioxus_core::prelude::*;
use generational_box::Storage;
use crate::dependency::Dependency;
use crate::use_signal;
use crate::{signal::SignalData, ReadOnlySignal, Signal};
/// Creates a new unsync Selector. The selector will be run immediately and whenever any signal it reads changes.
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App() -> Element {
/// let mut count = use_signal(|| 0);
/// let double = use_memo(move || count * 2);
/// count += 1;
/// assert_eq!(double.value(), count * 2);
///
/// rsx! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_memo<R: PartialEq>(f: impl FnMut() -> R + 'static) -> ReadOnlySignal<R> {
use_maybe_sync_memo(f)
}
/// Creates a new Selector that may be sync. The selector will be run immediately and whenever any signal it reads changes.
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut count = use_signal(cx, || 0);
/// let double = use_memo(cx, move || count * 2);
/// count += 1;
/// assert_eq!(double.value(), count * 2);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_maybe_sync_memo<R: PartialEq, S: Storage<SignalData<R>>>(
f: impl FnMut() -> R + 'static,
) -> ReadOnlySignal<R, S> {
use_hook(|| Signal::maybe_sync_memo(f))
}
/// Creates a new unsync Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut local_state = use_state(cx, || 0);
/// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
/// local_state.set(1);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_memo_with_dependencies<R: PartialEq, D: Dependency>(
dependencies: D,
f: impl FnMut(D::Out) -> R + 'static,
) -> ReadOnlySignal<R>
where
D::Out: 'static,
{
use_maybe_sync_selector_with_dependencies(dependencies, f)
}
/// Creates a new Selector that may be sync with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
///
/// Selectors can be used to efficiently compute derived data from signals.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut local_state = use_state(cx, || 0);
/// let double = use_memo_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
/// local_state.set(1);
///
/// render! { "{double}" }
/// }
/// ```
#[track_caller]
pub fn use_maybe_sync_selector_with_dependencies<
R: PartialEq,
D: Dependency,
S: Storage<SignalData<R>>,
>(
dependencies: D,
mut f: impl FnMut(D::Out) -> R + 'static,
) -> ReadOnlySignal<R, S>
where
D::Out: 'static,
{
let mut dependencies_signal = use_signal(|| dependencies.out());
let selector = use_hook(|| {
Signal::maybe_sync_memo(move || {
let deref = &*dependencies_signal.read();
f(deref.clone())
})
});
let changed = { dependencies.changed(&*dependencies_signal.read()) };
if changed {
dependencies_signal.set(dependencies.out());
}
selector
}

View file

@ -1,7 +1,15 @@
use crate::{
read::Readable, write::Writable, Effect, EffectInner, GlobalMemo, GlobalSignal, MappedSignal,
ReadOnlySignal,
get_effect_ref, read::Readable, write::Writable, CopyValue, Effect, EffectInner,
EffectStackRef, GlobalMemo, GlobalSignal, MappedSignal, ReadOnlySignal, EFFECT_STACK,
};
use dioxus_core::{
prelude::{
current_scope_id, has_context, provide_context, schedule_update_any, IntoAttributeValue,
},
ScopeId,
};
use generational_box::{AnyStorage, GenerationalBoxId, Storage, SyncStorage, UnsyncStorage};
use parking_lot::RwLock;
use std::{
any::Any,
cell::RefCell,
@ -10,154 +18,6 @@ use std::{
sync::Arc,
};
use dioxus_core::{
prelude::{
current_scope_id, has_context, provide_context, schedule_update_any, use_hook,
IntoAttributeValue,
},
ScopeId,
};
use generational_box::{AnyStorage, GenerationalBoxId, Storage, SyncStorage, UnsyncStorage};
use parking_lot::RwLock;
use crate::{get_effect_ref, CopyValue, EffectStackRef, EFFECT_STACK};
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App() -> Element {
/// let mut count = use_signal(|| 0);
///
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
/// // The app component will never be rerendered in this example.
/// rsx! { Child { state: count } }
/// }
///
/// #[component]
/// fn Child(state: Signal<u32>) -> Element {
/// let state = *state;
///
/// use_future( |()| async move {
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
/// *state.write() += 1;
/// });
///
/// rsx! {
/// button {
/// onclick: move |_| *state.write() += 1,
/// "{state}"
/// }
/// }
/// }
/// ```
#[track_caller]
#[must_use]
pub fn use_signal<T: 'static>(f: impl FnOnce() -> T) -> Signal<T, UnsyncStorage> {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
use_hook(|| {
Signal::new_with_caller(
f(),
#[cfg(debug_assertions)]
caller,
)
})
}
/// Creates a new `Send + Sync`` Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_signals::*;
///
/// fn App(cx: Scope) -> Element {
/// let mut count = use_signal_sync(cx, || 0);
///
/// // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
/// // The app component will never be rerendered in this example.
/// render! { Child { state: count } }
/// }
///
/// #[component]
/// fn Child(cx: Scope, state: Signal<u32, SyncStorage>) -> Element {
/// let state = *state;
///
/// use_future!(cx, |()| async move {
/// // This signal is Send + Sync, so we can use it in an another thread
/// tokio::spawn(async move {
/// // Because the signal is a Copy type, we can use it in an async block without cloning it.
/// *state.write() += 1;
/// }).await;
/// });
///
/// render! {
/// button {
/// onclick: move |_| *state.write() += 1,
/// "{state}"
/// }
/// }
/// }
/// ```
#[must_use]
#[track_caller]
pub fn use_signal_sync<T: Send + Sync + 'static>(f: impl FnOnce() -> T) -> Signal<T, SyncStorage> {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
use_hook(|| {
Signal::new_with_caller(
f(),
#[cfg(debug_assertions)]
caller,
)
})
}
struct Unsubscriber {
scope: ScopeId,
subscribers: UnsubscriberArray,
}
type UnsubscriberArray = Vec<Rc<RefCell<Vec<ScopeId>>>>;
impl Drop for Unsubscriber {
fn drop(&mut self) {
for subscribers in &self.subscribers {
subscribers.borrow_mut().retain(|s| *s != self.scope);
}
}
}
fn current_unsubscriber() -> Rc<RefCell<Unsubscriber>> {
match has_context() {
Some(rt) => rt,
None => {
let owner = Unsubscriber {
scope: current_scope_id().expect("in a virtual dom"),
subscribers: Default::default(),
};
provide_context(Rc::new(RefCell::new(owner)))
}
}
}
#[derive(Default)]
pub(crate) struct SignalSubscribers {
pub(crate) subscribers: Vec<ScopeId>,
pub(crate) effect_subscribers: Vec<GenerationalBoxId>,
}
/// The data stored for tracking in a signal.
pub struct SignalData<T> {
pub(crate) subscribers: Arc<RwLock<SignalSubscribers>>,
pub(crate) update_any: Arc<dyn Fn(ScopeId) + Sync + Send>,
pub(crate) effect_ref: EffectStackRef,
pub(crate) value: T,
}
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
///
/// ```rust
@ -197,22 +57,18 @@ pub struct Signal<T: 'static, S: Storage<SignalData<T>> = UnsyncStorage> {
/// A signal that can safely shared between threads.
pub type SyncSignal<T> = Signal<T, SyncStorage>;
#[cfg(feature = "serde")]
impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>>> serde::Serialize
for Signal<T, Store>
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.read().serialize(serializer)
}
/// The data stored for tracking in a signal.
pub struct SignalData<T> {
pub(crate) subscribers: Arc<RwLock<SignalSubscribers>>,
pub(crate) update_any: Arc<dyn Fn(ScopeId) + Sync + Send>,
pub(crate) effect_ref: EffectStackRef,
pub(crate) value: T,
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>>>
serde::Deserialize<'de> for Signal<T, Store>
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new_maybe_sync(T::deserialize(deserializer)?))
}
#[derive(Default)]
pub(crate) struct SignalSubscribers {
pub(crate) subscribers: Vec<ScopeId>,
pub(crate) effect_subscribers: Vec<GenerationalBoxId>,
}
impl<T: 'static> Signal<T> {
@ -312,7 +168,7 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
}
/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
fn new_with_caller(
pub fn new_with_caller(
value: T,
#[cfg(debug_assertions)] caller: &'static std::panic::Location<'static>,
) -> Self {
@ -386,6 +242,15 @@ impl<T: 'static, S: Storage<SignalData<T>>> Signal<T, S> {
}
}
pub fn unsubscribe(&self, scope: ScopeId) {
self.inner
.read()
.subscribers
.write()
.subscribers
.retain(|s| *s != scope);
}
/// Map the signal to a new type.
pub fn map<O>(self, f: impl Fn(&T) -> &O + 'static) -> MappedSignal<S::Ref<O>> {
MappedSignal::new(self, f)
@ -411,7 +276,8 @@ impl<T, S: Storage<SignalData<T>>> Readable<T> for Signal<T, S> {
S::try_map(ref_, f)
}
/// Get the current value of the signal. This will subscribe the current scope to the signal. If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead.
/// Get the current value of the signal. This will subscribe the current scope to the signal.
/// If you would like to read the signal without subscribing to it, you can use [`Self::peek`] instead.
///
/// If the signal has been dropped, this will panic.
#[track_caller]
@ -509,6 +375,52 @@ impl<T: Clone, S: Storage<SignalData<T>> + 'static> Deref for Signal<T, S> {
}
}
#[cfg(feature = "serde")]
impl<T: serde::Serialize + 'static, Store: Storage<SignalData<T>>> serde::Serialize
for Signal<T, Store>
{
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.read().serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de> + 'static, Store: Storage<SignalData<T>>>
serde::Deserialize<'de> for Signal<T, Store>
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Self::new_maybe_sync(T::deserialize(deserializer)?))
}
}
struct Unsubscriber {
scope: ScopeId,
subscribers: UnsubscriberArray,
}
type UnsubscriberArray = Vec<Rc<RefCell<Vec<ScopeId>>>>;
impl Drop for Unsubscriber {
fn drop(&mut self) {
for subscribers in &self.subscribers {
subscribers.borrow_mut().retain(|s| *s != self.scope);
}
}
}
fn current_unsubscriber() -> Rc<RefCell<Unsubscriber>> {
match has_context() {
Some(rt) => rt,
None => {
let owner = Unsubscriber {
scope: current_scope_id().expect("in a virtual dom"),
subscribers: Default::default(),
};
provide_context(Rc::new(RefCell::new(owner)))
}
}
}
struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
signal: Signal<T, S>,
}