Stores: Externally tracked state and reactive collections (#4483)

* 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

* writable is child owned

* box selector

* read and write to builtins

* re-use the proc macro in the derive macro

* Switch to a extension trait based approach

* switch to extension traits in the derive macro

* transposed method

* derive enums

* Expose Store::new

* remove stores from the prelude

* More consistent naming scheme for SelectorScope methods

* document hashmap methods

* document index extension

* document option ext

* document result ext

* document slice extension

* document the vec extension

* make more methods borrow the store mutably

* Move to impls to have higher priority over the builtin write extension traits

* fix deadlock

* finish stores documentation

* Document the store macro

* cleanup docs and organization

* finish derive docs

* fix typo

* fix clippy

* fix doc tests

* fix derive macro with generics

* fix clippy

* test and fix empty structs

* clean up todomvc store example

* default to a boxed store writer to make it easier to type when providing to the context api

* fix cargo doc

* fix clippy in store example

* relax generational box lifetime bounds

* relax bounds

* relax signal bounds

* fix compilation

* read store

* relax bounds on default store impls

* fix enum derive

* Convert to readsignal and writesignal automatically

* simplify type matching logic

* add write store

* unsized stores

* shrink the path vec type

* use more concrete types

* remove opaque types

* remove copy bounds

* use lens as generic name everywhere

* more consistent borrow semantics

* use a concrete type for the signal metadata and mention references in the map docs

* improve todomvc store docs

* document store derive macro internals

* refactor macro

* improve selector node documentation

* fix clippy

* add btreemap

* fix doc tests

* more option methods

* fix doc

* fix clippy

* add a bunch more result methods

* add a bunch of bounds to the iterators

* fix clippy

* remove option from subscribers

* add store macro to generate extension traits

* fix store example

* fix deadlock

* fix docs

* fix doc tests

* fix clippy

* add stores to public api

* add store macro, and then update todomvc_store example

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2025-08-05 11:30:21 -07:00
1 parent 5591070e92
commit 58c41f8561
57 files changed
+4958 -436

No files matched your search

+24 -34
View File
@@ -13,7 +13,7 @@ use syn::spanned::Spanned;
use syn::{parse::Error, PathArguments};
use quote::quote;
use syn::{parse_quote, GenericArgument, PathSegment, Type};
use syn::{parse_quote, GenericArgument, Ident, PathSegment, Type};
pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
let data = match &ast.data {
@@ -172,7 +172,7 @@ mod util {
}
mod field_info {
use crate::props::{looks_like_write_type, type_from_inside_option};
use crate::props::{looks_like_store_type, looks_like_write_type, type_from_inside_option};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
@@ -221,8 +221,8 @@ mod field_info {
builder_attr.auto_into = false;
}
// Write fields automatically use impl Into
if looks_like_write_type(&field.ty) {
// Write and Store fields automatically use impl Into
if looks_like_write_type(&field.ty) || looks_like_store_type(&field.ty) {
builder_attr.auto_into = true;
}
@@ -1744,44 +1744,34 @@ fn child_owned_type(ty: &Type) -> bool {
looks_like_signal_type(ty) || looks_like_write_type(ty) || looks_like_callback_type(ty)
}
/// Check if the path without generics matches the type we are looking for
fn last_segment_matches(ty: &Type, expected: &Ident) -> bool {
extract_base_type_without_generics(ty).is_some_and(|path_without_generics| {
path_without_generics
.segments
.last()
.is_some_and(|seg| seg.ident == *expected)
})
}
fn looks_like_signal_type(ty: &Type) -> bool {
match extract_base_type_without_generics(ty) {
Some(path_without_generics) => {
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,
}
last_segment_matches(ty, &parse_quote!(ReadOnlySignal))
|| last_segment_matches(ty, &parse_quote!(ReadSignal))
}
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,
}
last_segment_matches(ty, &parse_quote!(WriteSignal))
}
fn looks_like_store_type(ty: &Type) -> bool {
last_segment_matches(ty, &parse_quote!(Store))
|| last_segment_matches(ty, &parse_quote!(ReadStore))
}
fn looks_like_callback_type(ty: &Type) -> bool {
let type_without_option = remove_option_wrapper(ty.clone());
match extract_base_type_without_generics(&type_without_option) {
Some(path_without_generics) => {
path_without_generics == parse_quote!(dioxus_core::EventHandler)
|| path_without_generics == parse_quote!(prelude::EventHandler)
|| path_without_generics == parse_quote!(EventHandler)
|| path_without_generics == parse_quote!(dioxus_core::Callback)
|| path_without_generics == parse_quote!(prelude::Callback)
|| path_without_generics == parse_quote!(Callback)
}
None => false,
}
last_segment_matches(&type_without_option, &parse_quote!(EventHandler))
|| last_segment_matches(&type_without_option, &parse_quote!(Callback))
}
#[test]