Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d3d7ec3
map signals mutably
ealmloff Jul 14, 2025
1f3ceab
boxed signals
ealmloff Jul 14, 2025
9a2eed3
remove mutable borrow type from boxed writable
ealmloff Jul 14, 2025
f13d655
improve map mut example
ealmloff Jul 14, 2025
4be3d17
restore boxed_mut helper
ealmloff Jul 14, 2025
01c9d0e
make boxed readable/writable types copy
ealmloff Jul 14, 2025
b608234
Move away from the ReadOnlySignal alias in examples
ealmloff Jul 14, 2025
2b5ef8e
automatically convert Write
ealmloff Jul 14, 2025
714fa5f
Fix some tests
ealmloff Jul 14, 2025
195b0bd
ReadSignal and WriteSignal
ealmloff Jul 15, 2025
bcfa52c
move read only signal alias
ealmloff Jul 15, 2025
94f2b58
make maps copy
ealmloff Jul 15, 2025
abb58b2
store prototype
ealmloff Jul 15, 2025
08cf20b
implement derive macro
ealmloff Jul 16, 2025
9b39c34
Handle generics in the derive macro
ealmloff Jul 16, 2025
af67e9e
Make Selector boxable
ealmloff Jul 16, 2025
d089c52
derive boxed conversion
ealmloff Jul 16, 2025
9fda03c
Get rid of store type
ealmloff Jul 16, 2025
83f6ec4
Remove hardcoded type from derive macro
ealmloff Jul 16, 2025
0437501
selector -> store
ealmloff Jul 16, 2025
23beada
Refactor store implementation
ealmloff Jul 16, 2025
c58ab37
more vec methods
ealmloff Jul 16, 2025
76e6151
todo mvc store example
ealmloff Jul 16, 2025
f2029a9
Implement Storable for HashMap
ealmloff Jul 16, 2025
7c974fd
fix write
ealmloff Jul 16, 2025
274c35e
restore sorting logic in todomvc
ealmloff Jul 16, 2025
0a066fa
derive readable and writable
ealmloff Jul 18, 2025
1c200eb
improve todomvc store example
ealmloff Jul 18, 2025
793e0f0
Merge branch 'remote-origin' into signal-map-mut
ealmloff Jul 24, 2025
be769bc
Result store
ealmloff Jul 24, 2025
502043a
Option store
ealmloff Jul 28, 2025
5d82568
collect dead subscribers
ealmloff Jul 28, 2025
8f09fc1
fix clippy and formatting
ealmloff Jul 28, 2025
62ec4c7
recognize some common foreign types automatically
ealmloff Jul 28, 2025
b08078e
fix foreign stores in props
ealmloff Jul 28, 2025
2c9757c
slice and array selectors
ealmloff Jul 28, 2025
83dea63
clean up store example
ealmloff Jul 29, 2025
094e1c1
remove stores
ealmloff Jul 29, 2025
243e37e
writable is child owned
ealmloff Jul 29, 2025
6500594
fix typo
ealmloff Jul 29, 2025
7792d95
fix clippy
ealmloff Jul 29, 2025
6860864
fix doc tests
ealmloff Jul 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ members = [
"packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization",
"packages/playwright-tests/wasm-split-harness",
"packages/playwright-tests/default-features-disabled",
"packages/playwright-tests/default-features-disabled"
]

[workspace.package]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl FromStr for Size {
}

#[component]
pub fn product_page(product_id: ReadOnlySignal<usize>) -> Element {
pub fn product_page(product_id: ReadSignal<usize>) -> Element {
let mut quantity = use_signal(|| 1);
let mut size = use_signal(Size::default);

Expand Down
6 changes: 3 additions & 3 deletions example-projects/fullstack-hackernews/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub fn App() -> Element {
}

#[component]
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
fn Homepage(story: ReadSignal<PreviewState>) -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") }
div { display: "flex", flex_direction: "row", width: "100%",
Expand Down Expand Up @@ -84,7 +84,7 @@ fn Stories() -> Element {
}

#[component]
fn StoryListing(story: ReadOnlySignal<i64>) -> Element {
fn StoryListing(story: ReadSignal<i64>) -> Element {
let story = use_server_future(move || get_story(story()))?;

let StoryItem {
Expand Down Expand Up @@ -167,7 +167,7 @@ impl Display for PreviewState {
}

#[component]
fn Preview(story: ReadOnlySignal<PreviewState>) -> Element {
fn Preview(story: ReadSignal<PreviewState>) -> Element {
let PreviewState {
active_story: Some(id),
} = story()
Expand Down
2 changes: 1 addition & 1 deletion examples/backgrounded_futures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
2 changes: 1 addition & 1 deletion examples/bevy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion examples/dog_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion examples/hash_fragment_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion examples/memo_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
4 changes: 2 additions & 2 deletions examples/query_segment_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions examples/router_resource.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand All @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions examples/signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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! {
Expand Down
10 changes: 5 additions & 5 deletions examples/todomvc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion examples/weather_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion examples/wgpu-texture/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
8 changes: 4 additions & 4 deletions packages/core-macro/docs/component.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ You can use the `#[props()]` attribute to modify the behavior of the props the c
Props also act slightly differently when used with:

- [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`.
- [`ReadOnlySignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal<T>` when it is passed as a prop.
- [`ReadSignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal<T>` when it is passed as a prop.
- [`String`](#formatted-props) - The props macro will accept formatted strings for any prop field with the type `String`.
- [`children`](#children-props) - The props macro will accept child elements if you include the `children` prop.

Expand Down Expand Up @@ -257,14 +257,14 @@ fn Counter(count: i32) -> Element {

To fix this issue you can either:

1. Make the prop reactive by wrapping it in `ReadOnlySignal` (recommended):
1. Make the prop reactive by wrapping it in `ReadSignal` (recommended):

`ReadOnlySignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadOnlySignal` when it is passed as a prop.
`ReadSignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadSignal` when it is passed as a prop.

```rust, no_run
# use dioxus::prelude::*;
#[component]
fn Counter(count: ReadOnlySignal<i32>) -> Element {
fn Counter(count: ReadSignal<i32>) -> Element {
// Since we made count reactive, the memo will automatically rerun when count changes.
let doubled_count = use_memo(move || count() * 2);
rsx! {
Expand Down
8 changes: 4 additions & 4 deletions packages/core-macro/docs/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ You can use the `#[props()]` attribute to modify the behavior of the props deriv
Props also act slightly differently when used with:

- [`Option<T>`](#optional-props) - The field is automatically optional with a default value of `None`.
- [`ReadOnlySignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadOnlySignal<T>` when it is passed as a prop.
- [`ReadSignal<T>`](#reactive-props) - The props macro will automatically convert `T` into `ReadSignal<T>` when it is passed as a prop.
- [`String`](#formatted-props) - The props macro will accept formatted strings for any prop field with the type `String`.
- [`children`](#children-props) - The props macro will accept child elements if you include the `children` prop.

Expand Down Expand Up @@ -270,14 +270,14 @@ fn Counter(count: i32) -> Element {

To fix this issue you can either:

1. Make the prop reactive by wrapping it in `ReadOnlySignal` (recommended):
1. Make the prop reactive by wrapping it in `ReadSignal` (recommended):

`ReadOnlySignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadOnlySignal` when it is passed as a prop.
`ReadSignal` is a `Copy` reactive value. Dioxus will automatically convert any value into a `ReadSignal` when it is passed as a prop.

```rust, no_run
# use dioxus::prelude::*;
#[component]
fn Counter(count: ReadOnlySignal<i32>) -> Element {
fn Counter(count: ReadSignal<i32>) -> Element {
// Since we made count reactive, the memo will automatically rerun when count changes.
let doubled_count = use_memo(move || count() * 2);
rsx! {
Expand Down
42 changes: 36 additions & 6 deletions packages/core-macro/src/props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ mod util {
}

mod field_info {
use crate::props::type_from_inside_option;
use crate::props::{looks_like_write_type, type_from_inside_option};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
Expand Down Expand Up @@ -221,6 +221,11 @@ mod field_info {
builder_attr.auto_into = false;
}

// Write and Store fields automatically use impl Into
if looks_like_write_type(&field.ty) {
builder_attr.auto_into = true;
}

// extended field is automatically empty
if !builder_attr.extends.is_empty() {
builder_attr.default = Some(
Expand Down Expand Up @@ -503,9 +508,9 @@ fn type_from_inside_option(ty: &Type) -> Option<&Type> {
let seg = path.segments.last()?;

// If the segment is a supported optional type, provide the inner type.
// Return the inner type if the pattern is `Option<T>` or `ReadOnlySignal<Option<T>>``
if seg.ident == "ReadOnlySignal" {
// Get the inner type. E.g. the `u16` in `ReadOnlySignal<u16>` or `Option` in `ReadOnlySignal<Option<bool>>`
// Return the inner type if the pattern is `Option<T>` or `ReadSignal<Option<T>>``
if seg.ident == "ReadOnlySignal" || seg.ident == "ReadSignal" {
// Get the inner type. E.g. the `u16` in `ReadSignal<u16>` or `Option` in `ReadSignal<Option<bool>>`
let inner_type = extract_inner_type_from_segment(seg)?;
let Type::Path(inner_path) = inner_type else {
// If it isn't a path, the inner type isn't option
Expand Down Expand Up @@ -614,13 +619,13 @@ mod struct_info {
generics
}

/// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadOnlySignal<T>`, the new signal should be owned by the child
/// Checks if the props have any fields that should be owned by the child. For example, when converting T to `ReadSignal<T>`, the new signal should be owned by the child
fn has_child_owned_fields(&self) -> bool {
self.fields.iter().any(|f| child_owned_type(f.ty))
}

fn memoize_impl(&self) -> Result<TokenStream, Error> {
// First check if there are any ReadOnlySignal fields, if there are not, we can just use the partialEq impl
// First check if there are any ReadSignal fields, if there are not, we can just use the partialEq impl
let signal_fields: Vec<_> = self
.included_fields()
.filter(|f| looks_like_signal_type(f.ty))
Expand Down Expand Up @@ -1745,6 +1750,20 @@ fn looks_like_signal_type(ty: &Type) -> bool {
path_without_generics == parse_quote!(dioxus_core::ReadOnlySignal)
|| path_without_generics == parse_quote!(prelude::ReadOnlySignal)
|| path_without_generics == parse_quote!(ReadOnlySignal)
|| path_without_generics == parse_quote!(dioxus_core::prelude::ReadSignal)
|| path_without_generics == parse_quote!(prelude::ReadSignal)
|| path_without_generics == parse_quote!(ReadSignal)
}
None => false,
}
}

fn looks_like_write_type(ty: &Type) -> bool {
match extract_base_type_without_generics(ty) {
Some(path_without_generics) => {
path_without_generics == parse_quote!(dioxus_core::prelude::WriteSignal)
|| path_without_generics == parse_quote!(prelude::WriteSignal)
|| path_without_generics == parse_quote!(WriteSignal)
}
None => false,
}
Expand Down Expand Up @@ -1778,6 +1797,17 @@ fn test_looks_like_type() {
ReadOnlySignal<Option<i32>, UnsyncStorage>
)));

assert!(!looks_like_signal_type(&parse_quote!(
Option<ReadSignal<i32>>
)));
assert!(looks_like_signal_type(&parse_quote!(ReadSignal<i32>)));
assert!(looks_like_signal_type(
&parse_quote!(ReadSignal<i32, SyncStorage>)
));
assert!(looks_like_signal_type(&parse_quote!(
ReadSignal<Option<i32>, UnsyncStorage>
)));

assert!(looks_like_callback_type(&parse_quote!(
Option<EventHandler>
)));
Expand Down
16 changes: 8 additions & 8 deletions packages/core-macro/tests/rsx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ mod test_default_into {

// Test default values for signals
#[props(default)]
read_only_w_default: ReadOnlySignal<bool>,
read_only_w_default: ReadSignal<bool>,

#[props(default = true)]
read_only_w_default_val: ReadOnlySignal<bool>,
read_only_w_default_val: ReadSignal<bool>,

#[props(default = ReadOnlySignal::new(Signal::new(true)))]
read_only_w_default_val_explicit: ReadOnlySignal<bool>,
#[props(default = ReadSignal::new(Signal::new(true)))]
read_only_w_default_val_explicit: ReadSignal<bool>,

// Test default values for callbacks/event handlers
#[props(default)]
Expand Down Expand Up @@ -108,7 +108,7 @@ mod test_optional_signals {
fn UsesComponents() -> Element {
rsx! {
PropsStruct {
regular_read_signal: ReadOnlySignal::new(Signal::new(1234)),
regular_read_signal: ReadSignal::new(Signal::new(1234)),
}
PropsStruct {
optional_read_signal: 1234,
Expand All @@ -126,8 +126,8 @@ mod test_optional_signals {
// Test props as struct param.
#[derive(Props, Clone, PartialEq)]
struct MyTestProps {
pub optional_read_signal: ReadOnlySignal<Option<u16>>,
pub regular_read_signal: ReadOnlySignal<u16>,
pub optional_read_signal: ReadSignal<Option<u16>>,
pub regular_read_signal: ReadSignal<u16>,
}

#[component]
Expand All @@ -137,7 +137,7 @@ mod test_optional_signals {

// Test props as params.
#[component]
fn PropParams(opt_read_sig: ReadOnlySignal<Option<u16>>) -> Element {
fn PropParams(opt_read_sig: ReadSignal<Option<u16>>) -> Element {
rsx! { "hi!" }
}

Expand Down
Loading