# Dioxus Signals vs Stores: State Management Best Practices

> Master Dioxus state management. Learn best practices for Signals and Stores, choosing the right tool for simple or complex data. Optimize your Dioxus app reactivity.

- Repository: [Dioxus Labs/dioxus](https://github.com/DioxusLabs/dioxus)
- Tags: best-practices
- Published: 2026-07-23

---

**Use Signals for simple copy-on-write primitives with automatic reactivity, and Stores for complex mutable data structures requiring fine-grained subscriptions.**

Dioxus provides two complementary primitives for managing reactive state in Rust applications. Understanding when to use **Signals** versus **Stores** is essential for building performant, maintainable UIs with the DioxusLabs/dioxus framework. Choosing the right abstraction prevents unnecessary re-renders and ensures type-safe state sharing across components.

## When to Use Signals in Dioxus

**Signals** are copy-on-write values that automatically track which components read them. When the value changes, all subscribing scopes re-run immediately.

### Automatic Dependency Tracking

In [`packages/signals/src/signal.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/signals/src/signal.rs), the `Signal` type implements automatic subscription tracking through the `read()` method. When a component calls `*signal.read()`, the current scope registers itself as a subscriber. Subsequent calls to `*signal.write()` automatically notify all registered scopes to re-render.

Signals excel with **copyable primitives**: integers, booleans, strings, and small immutable data. They use a macro-generated `CopyValue` wrapper that avoids heap allocations, making them extremely lightweight for frequent updates.

### Thread Safety Considerations

By default, `Signal<T>` uses `UnsyncStorage` and is restricted to the creating thread. For cross-thread access, use `SyncSignal<T>` (aliased as `Signal<T, SyncStorage>`). Accessing an unsynced signal from another thread will panic at runtime.

```rust
use dioxus::prelude::*;

// Local component state
#[component]
fn Counter() -> Element {
    let count = Signal::new(0);
    
    rsx! {
        button {
            onclick: move |_| *count.write() += 1,
            "Count: {count}"
        }
    }
}

// Global app-wide state
static GLOBAL_COUNT: GlobalSignal<i32> = Signal::global(|| 0);

#[component]
fn GlobalCounter() -> Element {
    rsx! {
        button {
            onclick: move |_| *GLOBAL_COUNT.write() += 1,
            "Global: {GLOBAL_COUNT}"
        }
    }
}

```

## When to Use Stores for Complex State

**Stores** are containers for complex data that implement the `Subscriptions` trait. Defined in [`packages/stores/src/store.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/stores/src/store.rs), they expose fine-grained APIs like `subscribe` and `listen` for precise control over update notifications.

### Fine-Grained Subscriptions

Unlike Signals, which notify all subscribers on any change, Stores allow **partial updates**. In [`packages/stores/src/subscriptions.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/stores/src/subscriptions.rs), the subscription logic ensures only listeners monitoring specific fields receive notifications. This prevents cascading re-renders when unrelated data changes.

### Handling Mutable Collections

Stores are ideal for `Vec<T>`, `HashMap<K, V>`, or large structs. Because they mutate in place rather than cloning the entire container, they remain efficient for large datasets that change frequently.

```rust
use dioxus::prelude::*;
use std::sync::Arc;

#[derive(Default, Clone)]
struct TodoList {
    items: Vec<String>,
    filter: String,
}

#[component]
fn TodoApp() -> Element {
    let store = use_store(|| Store::new(TodoList::default()));
    
    // Subscribes to entire store
    let todos = store.read().items.clone();
    
    rsx! {
        ul { 
            todos.iter().map(|t| rsx!{ li { "{t}" } })
        }
        input {
            oninput: move |e| {
                // Mutates in place without cloning the vector
                store.write().items.push(e.value.clone());
            }
        }
    }
}

// Partial subscription - only re-renders when items length changes
#[component]
fn TodoCount() -> Element {
    let store = use_store::<TodoList>();
    let count = store.listen(|s| s.items.len());
    
    rsx! { div { "Items: {count}" } }
}

```

## Comparing Signals and Stores

| Aspect | Signal | Store |
|--------|--------|-------|
| **Concept** | Copy-on-write with automatic tracking | Mutable container with explicit subscription |
| **Best For** | Primitives, simple state | Collections, complex structs, async data |
| **Reading** | `*signal.read()` subscribes automatically | `store.read().field` (full subscription) or `store.listen()` (partial) |
| **Writing** | `*signal.write() = value` | `store.write().field = value` |
| **Performance** | Zero-cost for copyable types | Efficient for large mutable structures |
| **Thread Safety** | Requires `SyncSignal` for cross-thread | Always thread-safe with `Send + Sync` types |

## Best Practices for Dioxus State Management

- **Start with Signals** for copyable primitives like counters, flags, and text inputs. They provide the least boilerplate and most ergonomic reactivity.

- **Upgrade to Stores** when handling non-copyable types like `Vec<T>` or `HashMap<K, V>`, or when you need fine-grained subscriptions to specific fields.

- **Avoid `write_silent`** on Signals. This legacy method disables automatic updates and causes UI drift. Use `peek()` for non-reactive reads instead.

- **Manage scope ownership** carefully. Signals created inside `use_signal` drop when the component unmounts. For long-lived state, use `Signal::global` or provide the signal via `use_context_provider` in a higher-level component.

- **Use Memoization** for derived data. Create computed values with `Memo::new` (defined in [`packages/signals/src/memo.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/signals/src/memo.rs)) rather than duplicating logic across multiple Signals.

- **Select proper thread safety**. Use `SyncSignal` or `Store` with `SyncStorage` when mutating state from background threads. Default Signals will panic if accessed across thread boundaries.

- **Debug effectively** using `peek()` to inspect Signal values without triggering re-renders, and `store.listen()` callbacks to log subscription changes.

## Summary

- **Signals** in [`packages/signals/src/signal.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/signals/src/signal.rs) provide zero-boilerplate reactivity for simple, copyable types through automatic dependency tracking.
- **Stores** in [`packages/stores/src/store.rs`](https://github.com/DioxusLabs/dioxus/blob/main/packages/stores/src/store.rs) offer fine-grained control over complex, mutable state via the subscription system defined in [`subscriptions.rs`](https://github.com/DioxusLabs/dioxus/blob/main/subscriptions.rs).
- Start with Signals for local UI state and migrate to Stores when you need partial updates, mutable collections, or cross-thread access.
- Always match your storage type—`UnsyncStorage` for single-threaded use, `SyncStorage` for shared thread access—to your application's concurrency requirements.

## Frequently Asked Questions

### Can I use both Signals and Stores in the same component?

Yes. Many applications combine both primitives: Signals for local UI state like form inputs or toggles, and Stores for shared application state like data collections or cached API responses. Import both `use_signal` and `use_store` hooks to manage different state lifecycles within a single component.

### Why does my Signal panic when accessed from a background thread?

Standard `Signal<T>` uses `UnsyncStorage` by default, restricting access to the creating thread. To share state across threads, use `SyncSignal<T>` (or `Signal<T, SyncStorage>`). Alternatively, use a `Store` which is inherently thread-safe when the underlying type implements `Send + Sync`.

### How do I prevent unnecessary re-renders with large datasets?

Use a `Store` instead of a `Signal`. While Signals clone the entire value on every write, Stores mutate data in place and only notify specific subscribers. Use `store.listen(|state| state.specific_field)` to subscribe to individual fields rather than the entire structure, minimizing render cycles.

### What is the difference between `read()` and `peek()` in Dioxus Signals?

The `read()` method subscribes the current scope to future updates, causing the component to re-render when the value changes. The `peek()` method retrieves the current value without establishing a subscription, useful for debugging or conditional logic that should not trigger reactive updates.