Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 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
d4d7ec1
writable is child owned
ealmloff Jul 29, 2025
51f5ecc
box selector
ealmloff Jul 29, 2025
26dfce5
read and write to builtins
ealmloff Jul 29, 2025
7734244
re-use the proc macro in the derive macro
ealmloff Jul 29, 2025
b0ad9eb
Switch to a extension trait based approach
ealmloff Jul 29, 2025
70553c2
switch to extension traits in the derive macro
ealmloff Jul 30, 2025
fdd7126
transposed method
ealmloff Jul 30, 2025
286e444
derive enums
ealmloff Jul 30, 2025
4139d42
Expose Store::new
ealmloff Jul 30, 2025
ee6070b
remove stores from the prelude
ealmloff Jul 30, 2025
102ec32
More consistent naming scheme for SelectorScope methods
ealmloff Jul 30, 2025
f884a0b
document hashmap methods
ealmloff Jul 30, 2025
db77061
document index extension
ealmloff Jul 30, 2025
baaa911
document option ext
ealmloff Jul 30, 2025
ad69234
document result ext
ealmloff Jul 30, 2025
c01b958
document slice extension
ealmloff Jul 30, 2025
d609664
document the vec extension
ealmloff Jul 30, 2025
c42e1c1
make more methods borrow the store mutably
ealmloff Jul 30, 2025
732b5c8
Move to impls to have higher priority over the builtin write extensio…
ealmloff Jul 30, 2025
1662fa5
fix deadlock
ealmloff Jul 30, 2025
7336aac
finish stores documentation
ealmloff Jul 30, 2025
8fc362b
Document the store macro
ealmloff Jul 30, 2025
71933f0
cleanup docs and organization
ealmloff Jul 30, 2025
ce90970
finish derive docs
ealmloff Jul 30, 2025
1f49036
fix typo
ealmloff Jul 29, 2025
bdf216c
fix clippy
ealmloff Jul 29, 2025
a69407e
fix doc tests
ealmloff Jul 29, 2025
619ad79
fix derive macro with generics
ealmloff Jul 30, 2025
98befc9
fix clippy
ealmloff Jul 30, 2025
31afbbf
test and fix empty structs
ealmloff Jul 30, 2025
83f28a3
clean up todomvc store example
ealmloff Jul 30, 2025
cb12564
default to a boxed store writer to make it easier to type when provid…
ealmloff Jul 30, 2025
864b061
fix cargo doc
ealmloff Jul 30, 2025
0a4e939
fix clippy in store example
ealmloff Jul 30, 2025
c2480c4
Merge branch 'remote-origin' into stores
ealmloff Jul 31, 2025
b34ae85
relax generational box lifetime bounds
ealmloff Aug 1, 2025
6dee9be
relax bounds
ealmloff Aug 1, 2025
2fe7b7a
relax signal bounds
ealmloff Aug 1, 2025
6d54702
fix compilation
ealmloff Aug 1, 2025
b098f5c
read store
ealmloff Aug 1, 2025
5df1ad6
relax bounds on default store impls
ealmloff Aug 1, 2025
2d5fd8e
fix enum derive
ealmloff Aug 1, 2025
8479be4
Convert to readsignal and writesignal automatically
ealmloff Aug 1, 2025
a18ee31
simplify type matching logic
ealmloff Aug 1, 2025
5345adb
add write store
ealmloff Aug 1, 2025
c54c296
unsized stores
ealmloff Aug 1, 2025
f8d81c0
shrink the path vec type
ealmloff Aug 1, 2025
b394024
use more concrete types
ealmloff Aug 1, 2025
45e725d
remove opaque types
ealmloff Aug 1, 2025
13a76e8
remove copy bounds
ealmloff Aug 1, 2025
5f9c8d9
use lens as generic name everywhere
ealmloff Aug 1, 2025
72deed9
more consistent borrow semantics
ealmloff Aug 1, 2025
750348a
use a concrete type for the signal metadata and mention references in…
ealmloff Aug 1, 2025
607a3c3
improve todomvc store docs
ealmloff Aug 1, 2025
3bb878d
document store derive macro internals
ealmloff Aug 1, 2025
506d46c
refactor macro
ealmloff Aug 1, 2025
92f44e4
improve selector node documentation
ealmloff Aug 1, 2025
1b13504
fix clippy
ealmloff Aug 1, 2025
931b435
add btreemap
ealmloff Aug 1, 2025
0922661
fix doc tests
ealmloff Aug 1, 2025
3dbfb08
more option methods
ealmloff Aug 1, 2025
aed4792
fix doc
ealmloff Aug 1, 2025
b339de6
fix clippy
ealmloff Aug 1, 2025
06d84d5
add a bunch more result methods
ealmloff Aug 1, 2025
bdb4d35
add a bunch of bounds to the iterators
ealmloff Aug 1, 2025
e0135dc
fix clippy
ealmloff Aug 1, 2025
3d1467a
remove option from subscribers
ealmloff Aug 4, 2025
7b0a718
add store macro to generate extension traits
ealmloff Aug 4, 2025
72a836a
fix store example
ealmloff Aug 4, 2025
2ae33d2
fix deadlock
ealmloff Aug 4, 2025
8eadeb3
fix docs
ealmloff Aug 4, 2025
f100832
fix doc tests
ealmloff Aug 4, 2025
fae8d9f
fix clippy
ealmloff Aug 4, 2025
3022c70
add stores to public api
jkelleyrtp Aug 5, 2025
4984d36
add store macro, and then update todomvc_store example
jkelleyrtp Aug 5, 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
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ members = [
"packages/rsx",
"packages/server-macro",
"packages/signals",
"packages/stores",
"packages/stores-macro",
"packages/ssr",
"packages/lazy-js-bundle",
"packages/cli-config",
Expand Down Expand Up @@ -161,6 +163,8 @@ dioxus-rsx = { path = "packages/rsx", version = "0.7.0-alpha.3" }
dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.7.0-alpha.3" }
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.7.0-alpha.3" }
dioxus-signals = { path = "packages/signals", version = "0.7.0-alpha.3" }
dioxus-stores = { path = "packages/stores", version = "0.7.0-alpha.3" }
dioxus-stores-macro = { path = "packages/stores-macro", version = "0.7.0-alpha.3" }
dioxus-cli-config = { path = "packages/cli-config", version = "0.7.0-alpha.3" }
dioxus-cli-opt = { path = "packages/cli-opt", version = "0.7.0-alpha.3" }
dioxus-devtools = { path = "packages/devtools", version = "0.7.0-alpha.3" }
Expand Down Expand Up @@ -427,6 +431,7 @@ wasm-splitter = { workspace = true, package = "wasm-split" }

[dev-dependencies]
dioxus = { workspace = true, features = ["router"] }
dioxus-stores = { workspace = true }
dioxus-ssr = { workspace = true }
futures-util = { workspace = true }
separator = { workspace = true }
Expand Down
313 changes: 313 additions & 0 deletions examples/todomvc_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
//! The typical TodoMVC app, implemented in Dioxus with stores. Stores let us
//! share nested reactive state between components. They let us keep our todomvc
//! state in a single struct without wrapping every type in a signal while still
//! maintaining fine grained reactivity.

use dioxus::prelude::*;
use std::{collections::HashMap, vec};

const STYLE: Asset = asset!("/examples/assets/todomvc.css");

/// Deriving the store macro on a struct will automatically generate an extension trait
/// for Store<TodoState> with method to zoom into the fields of the struct.
///
/// For this struct, the macro derives the following methods for Store<TodoState>:
/// - `todos(self) -> Store<HashMap<u32, TodoItem>, _>`
/// - `filter(self) -> Store<FilterState, _>`
#[derive(Store, PartialEq, Clone, Debug)]
struct TodoState {
todos: HashMap<u32, TodoItem>,
filter: FilterState,
}

// We can also add custom methods to the store by using the `store` attribute on an impl block.
// The store attribute turns the impl block into an extension trait for Store<TodoState>.
// Methods that take &self will automatically get a bound that Lens: Readable<Target = TodoState>
// Methods that take &mut self will automatically get a bound that Lens: Writable<Target = TodoState>
#[store]
impl<Lens> Store<TodoState, Lens> {
fn active_items(&self) -> Vec<u32> {
let filter = self.filter().cloned();
let mut active_ids: Vec<u32> = self
.todos()
.iter()
.filter_map(|(id, item)| item.active(filter).then_some(id))
.collect();
active_ids.sort_unstable();
active_ids
}

fn incomplete_count(&self) -> usize {
self.todos()
.values()
.filter(|item| item.incomplete())
.count()
}

fn toggle_all(&mut self) {
let check = self.incomplete_count() != 0;
for item in self.todos().values() {
item.checked().set(check);
}
}

fn has_todos(&self) -> bool {
!self.todos().is_empty()
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum FilterState {
All,
Active,
Completed,
}

#[derive(Store, PartialEq, Clone, Debug)]
struct TodoItem {
checked: bool,
contents: String,
}

impl TodoItem {
fn new(contents: impl ToString) -> Self {
Self {
checked: false,
contents: contents.to_string(),
}
}
}

#[store]
impl<Lens> Store<TodoItem, Lens> {
fn complete(&self) -> bool {
self.checked().cloned()
}

fn incomplete(&self) -> bool {
!self.complete()
}

fn active(&self, filter: FilterState) -> bool {
match filter {
FilterState::All => true,
FilterState::Active => self.incomplete(),
FilterState::Completed => self.complete(),
}
}
}

fn main() {
dioxus::launch(app);
}

fn app() -> Element {
// We store the state of our todo list in a store to use throughout the app.
let mut todos = use_store(|| TodoState {
todos: HashMap::new(),
filter: FilterState::All,
});

// We use a simple memo to calculate the number of active todos.
// Whenever the todos change, the active_todo_count will be recalculated.
let active_todo_count = use_memo(move || todos.incomplete_count());

// We use a memo to filter the todos based on the current filter state.
// Whenever the todos or filter change, the filtered_todos will be recalculated.
// Note that we're only storing the IDs of the todos, not the todos themselves.
let filtered_todos = use_memo(move || todos.active_items());

// Toggle all the todos to the opposite of the current state.
// If all todos are checked, uncheck them all. If any are unchecked, check them all.
let toggle_all = move |_| {
todos.toggle_all();
};

rsx! {
document::Link { rel: "stylesheet", href: STYLE }
section { class: "todoapp",
TodoHeader { todos }
section { class: "main",
if todos.has_todos() {
input {
id: "toggle-all",
class: "toggle-all",
r#type: "checkbox",
onchange: toggle_all,
checked: active_todo_count() == 0
}
label { r#for: "toggle-all" }
}

// Render the todos using the filtered_todos memo
// We pass the ID along with the hashmap into the TodoEntry component so it can access the todo from the todos store.
ul { class: "todo-list",
for id in filtered_todos() {
TodoEntry { key: "{id}", id, todos }
}
}

// We only show the footer if there are todos.
if todos.has_todos() {
ListFooter { active_todo_count, todos }
}
}
}

// A simple info footer
footer { class: "info",
p { "Double-click to edit a todo" }
p {
"Created by "
a { href: "http://github.com/jkelleyrtp/", "jkelleyrtp" }
}
p {
"Part of "
a { href: "http://todomvc.com", "TodoMVC" }
}
}
}
}

#[component]
fn TodoHeader(mut todos: Store<TodoState>) -> Element {
let mut draft = use_signal(|| "".to_string());
let mut todo_id = use_signal(|| 0);

let onkeydown = move |evt: KeyboardEvent| {
if evt.key() == Key::Enter && !draft.read().is_empty() {
let id = todo_id();
let todo = TodoItem::new(draft.take());
todos.todos().insert(id, todo);
todo_id += 1;
}
};

rsx! {
header { class: "header",
h1 { "todos" }
input {
class: "new-todo",
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| draft.set(evt.value()),
onkeydown
}
}
}
}

/// A single todo entry
/// This takes the ID of the todo and the todos store as props
/// We can use these together to memoize the todo contents and checked state
#[component]
fn TodoEntry(mut todos: Store<TodoState>, id: u32) -> Element {
let mut is_editing = use_signal(|| false);

// When we get an item out of the store, it will only subscribe to that specific item.
// Since we only get the single todo item, the component will only rerender when that item changes.
let entry = todos.todos().get(id).unwrap();
let checked = entry.checked();
let contents = entry.contents();

rsx! {
li {
// Dioxus lets you use if statements in rsx to conditionally render attributes
// These will get merged into a single class attribute
class: if checked() { "completed" },
class: if is_editing() { "editing" },

// Some basic controls for the todo
div { class: "view",
input {
class: "toggle",
r#type: "checkbox",
id: "cbg-{id}",
checked: "{checked}",
oninput: move |evt| entry.checked().set(evt.checked())
}
label {
r#for: "cbg-{id}",
ondoubleclick: move |_| is_editing.set(true),
onclick: |evt| evt.prevent_default(),
"{contents}"
}
button {
class: "destroy",
onclick: move |evt| {
evt.prevent_default();
todos.todos().remove(&id);
},
}
}

// Only render the actual input if we're editing
if is_editing() {
input {
class: "edit",
value: "{contents}",
oninput: move |evt| entry.contents().set(evt.value()),
autofocus: "true",
onfocusout: move |_| is_editing.set(false),
onkeydown: move |evt| {
match evt.key() {
Key::Enter | Key::Escape | Key::Tab => is_editing.set(false),
_ => {}
}
}
}
}
}
}
}

#[component]
fn ListFooter(mut todos: Store<TodoState>, active_todo_count: ReadSignal<usize>) -> Element {
// We use a memo to calculate whether we should show the "Clear completed" button.
// This will recompute whenever the number of todos change or the checked state of an existing
// todo changes
let show_clear_completed = use_memo(move || todos.todos().values().any(|todo| todo.complete()));
let mut filter = todos.filter();

rsx! {
footer { class: "footer",
span { class: "todo-count",
strong { "{active_todo_count} " }
span {
match active_todo_count() {
1 => "item",
_ => "items",
}
" left"
}
}
ul { class: "filters",
for (state , state_text , url) in [
(FilterState::All, "All", "#/"),
(FilterState::Active, "Active", "#/active"),
(FilterState::Completed, "Completed", "#/completed"),
] {
li {
a {
href: url,
class: if filter() == state { "selected" },
onclick: move |evt| {
evt.prevent_default();
filter.set(state)
},
{state_text}
}
}
}
}
if show_clear_completed() {
button {
class: "clear-completed",
onclick: move |_| todos.todos().retain(|_, todo| !todo.checked),
"Clear completed"
}
}
}
}
}
Loading
Loading