Boxed and mapped signals preparing for stores (#4413)

This PR contains just the breaking changes required for the upcoming store implementation. It adds two new primitives:

Mutable mapped signals (without a different subscriber list). This lets you scope a writable value and pass it to a child component. It requires two closures to map the immutable and mutable references to the inner signal. With stores, these closures will be derived
Boxed readable and writable values (ReadSignal, and WriteSignal). Any readable or writable types can be converted to a read signal or write signal with the boxed method or built in types are automatically converted into the boxed variants when passing into props


* map signals mutably

* boxed signals

* remove mutable borrow type from boxed writable

* improve map mut example

* restore boxed_mut helper

* make boxed readable/writable types copy

* Move away from the ReadOnlySignal alias in examples

* automatically convert Write

* Fix some tests

* ReadSignal and WriteSignal

* move read only signal alias

* make maps copy

* store prototype

* implement derive macro

* Handle generics in the derive macro

* Make Selector boxable

* derive boxed conversion

* Get rid of store type

* Remove hardcoded type from derive macro

* selector -> store

* Refactor store implementation

* more vec methods

* todo mvc store example

* Implement Storable for HashMap

* fix write

* restore sorting logic in todomvc

* derive readable and writable

* improve todomvc store example

* Result store

* Option store

* collect dead subscribers

* fix clippy and formatting

* recognize some common foreign types automatically

* fix foreign stores in props

* slice and array selectors

* clean up store example

* remove stores

* writable is child owned

* fix typo

* fix clippy

* fix doc tests
This commit is contained in:
Evan Almloff authored and GitHub committed 2025-07-31 13:19:55 -07:00
1 parent 6919f72abc
commit d8d11db403
64 files changed
+1307 -726

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ fn app() -> Element {
}
#[component]
fn Child(count: Signal<i32>) -> Element {
fn Child(count: WriteSignal<i32>) -> Element {
let mut early_return = use_signal(|| false);
let early = rsx! {
+1 -1
View File
@@ -75,7 +75,7 @@ fn app() -> Element {
}
#[component]
fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element {
fn ColorControl(label: &'static str, color_str: WriteSignal<String>) -> Element {
rsx!(div {
class: "color-control",
{ label },
+1 -1
View File
@@ -60,7 +60,7 @@ fn app() -> Element {
}
#[component]
fn BreedPic(breed: Signal<String>) -> Element {
fn BreedPic(breed: WriteSignal<String>) -> Element {
// This resource will restart whenever the breed changes
let mut fut = use_resource(move || async move {
#[derive(serde::Deserialize, Debug)]
+1 -1
View File
@@ -81,7 +81,7 @@ impl FromStr for State {
}
#[component]
fn Home(url_hash: ReadOnlySignal<State>) -> Element {
fn Home(url_hash: ReadSignal<State>) -> Element {
// The initial state of the state comes from the url hash
let mut state = use_signal(&*url_hash);
+1 -1
View File
@@ -28,7 +28,7 @@ fn app() -> Element {
}
#[component]
fn Child(state: Memo<isize>, items: Memo<Vec<isize>>, depth: ReadOnlySignal<usize>) -> Element {
fn Child(state: Memo<isize>, items: Memo<Vec<isize>>, depth: ReadSignal<usize>) -> Element {
// These memos don't get re-computed when early returns happen
let state = use_memo(move || state() + 1);
let item = use_memo(move || items()[depth() - 1]);
+2 -2
View File
@@ -63,9 +63,9 @@ fn Home() -> Element {
}
}
// Instead of accepting String and usize directly, we use ReadOnlySignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme
// Instead of accepting String and usize directly, we use ReadSignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme
#[component]
fn Search(query: ReadOnlySignal<String>, word_count: ReadOnlySignal<usize>) -> Element {
fn Search(query: ReadSignal<String>, word_count: ReadSignal<usize>) -> Element {
const ITEMS: &[&str] = &[
"hello",
"world",
+5 -5
View File
@@ -1,7 +1,7 @@
//! Example: Updating components with use_resource
//! -----------------
//!
//! This example shows how to use ReadOnlySignal to make props reactive
//! This example shows how to use ReadSignal to make props reactive
//! when linking to it from the same component, when using use_resource
use dioxus::prelude::*;
@@ -25,15 +25,15 @@ fn App() -> Element {
}
}
// We use id: ReadOnlySignal<i32> instead of id: i32 to make id work with reactive hooks
// Any i32 we pass in will automatically be converted into a ReadOnlySignal<i32>
// We use id: ReadSignal<i32> instead of id: i32 to make id work with reactive hooks
// Any i32 we pass in will automatically be converted into a ReadSignal<i32>
#[component]
fn Blog(id: ReadOnlySignal<i32>) -> Element {
fn Blog(id: ReadSignal<i32>) -> Element {
async fn future(n: i32) -> i32 {
n
}
// Because we accept ReadOnlySignal<i32> instead of i32, the resource will automatically subscribe to the id when we read it
// Because we accept ReadSignal<i32> instead of i32, the resource will automatically subscribe to the id when we read it
let res = use_resource(move || future(id()));
match res() {
+2 -2
View File
@@ -3,7 +3,7 @@
//! This simple example implements a counter that can be incremented, decremented, and paused. It also demonstrates
//! that background tasks in use_futures can modify the value as well.
//!
//! Most signals implement Into<ReadOnlySignal<T>>, making ReadOnlySignal a good default type when building new
//! Most signals implement Into<ReadSignal<T>>, making ReadSignal a good default type when building new
//! library components that don't need to modify their values.
use async_std::task::sleep;
@@ -81,7 +81,7 @@ fn app() -> Element {
}
#[component]
fn Child(mut count: ReadOnlySignal<i32>) -> Element {
fn Child(mut count: ReadSignal<i32>) -> Element {
println!("rendering child with count {count}");
rsx! {
+5 -5
View File
@@ -110,7 +110,7 @@ fn app() -> Element {
}
#[component]
fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
fn TodoHeader(mut todos: WriteSignal<HashMap<u32, TodoItem>>) -> Element {
let mut draft = use_signal(|| "".to_string());
let mut todo_id = use_signal(|| 0);
@@ -146,7 +146,7 @@ fn TodoHeader(mut todos: Signal<HashMap<u32, TodoItem>>) -> Element {
/// This takes the ID of the todo and the todos signal as props
/// We can use these together to memoize the todo contents and checked state
#[component]
fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, id: u32) -> Element {
fn TodoEntry(mut todos: WriteSignal<HashMap<u32, TodoItem>>, id: u32) -> Element {
let mut is_editing = use_signal(|| false);
// To avoid re-rendering this component when the todo list changes, we isolate our reads to memos
@@ -208,9 +208,9 @@ fn TodoEntry(mut todos: Signal<HashMap<u32, TodoItem>>, id: u32) -> Element {
#[component]
fn ListFooter(
mut todos: Signal<HashMap<u32, TodoItem>>,
active_todo_count: ReadOnlySignal<usize>,
mut filter: Signal<FilterState>,
mut todos: WriteSignal<HashMap<u32, TodoItem>>,
active_todo_count: ReadSignal<usize>,
mut filter: WriteSignal<FilterState>,
) -> Element {
// We use a memoized signal to calculate whether we should show the "Clear completed" button.
// This will recompute whenever the todos change, and if the value is true, the button will be shown.
+1 -1
View File
@@ -108,7 +108,7 @@ fn Forecast(weather: WeatherResponse) -> Element {
}
#[component]
fn SearchBox(mut country: Signal<WeatherLocation>) -> Element {
fn SearchBox(mut country: WriteSignal<WeatherLocation>) -> Element {
let mut input = use_signal(|| "".to_string());
let locations = use_resource(move || async move { get_locations(&input()).await });
+1 -1
View File
@@ -87,7 +87,7 @@ fn app() -> Element {
}
#[component]
fn ColorControl(label: &'static str, color_str: Signal<String>) -> Element {
fn ColorControl(label: &'static str, color_str: WriteSignal<String>) -> Element {
rsx!(div {
class: "color-control",
{ label },