Skip to content

Dioxus

amethystate-dioxus bridges reactive state to Dioxus signals. Each field becomes a ReadSignal that re-renders only the components that read it. Handles are Copy arena-allocated indices, so they can be passed freely between components without cloning.

[dependencies]
amethystate-dioxus = "*"

Add #[amethystate_dioxus] above #[amethystate] on each struct you want to use in components:

use amethystate_dioxus::{amethystate_dioxus, amethystate};
#[amethystate_dioxus]
#[amethystate(prefix = "settings")]
pub struct AppSettings {
#[amestate(default = "Guest".to_string())]
pub username: String,
#[amestate(default = 0)]
pub counter: i32,
#[amestate(nested)]
pub theme: Theme,
}
#[amethystate_dioxus]
#[amethystate]
pub struct Theme {
#[amestate(default = "light".to_string())]
pub mode: String,
}

Wrap your app in amethystateProvider and pass the store:

#[component]
fn App() -> Element {
let store = use_hook(|| {
StoreBuilder::new("./app")
.build()
.expect("failed to open store")
});
rsx! {
amethystateProvider {
store,
Settings {}
}
}
}

use_amethystate::<S>() returns a Handle<S> — a Copy struct with a field for each state field. Pass it down to child components as a prop:

#[component]
fn Settings() -> Element {
let state = use_amethystate::<AppSettings>();
// state.username, state.counter, state.theme.mode — all Copy handles
}

Handle<S> is a type alias that hides the generated handle name. Declare props with it:

#[component]
fn ThemeEditor(settings: Handle<AppSettings>) -> Element {
// ...
}

Returns a (ReadSignal<T>, Callback<T>) pair for a writable field:

let (username, set_username) = use_field(state.username);
rsx! {
input {
value: "{username}",
oninput: move |e| set_username(e.value()),
}
}

Returns a ReadSignal<T> for a read-only field or a lookup field without export_mut:

let host = use_read_only_field(state.host);

Derives a signal from one or more fields. The pipeline is registered in the arena and cleaned up when the component unmounts:

let address = use_pipeline(move || {
(state.username, state.counter)
.pipe()
.map(|(u, c)| format!("{u}:{c}"))
.dedupe()
});
rsx! { p { "{address}" } }

Returns a MapSignal<K, V> for a writable ReactiveMap field. The signal holds the full snapshot and updates on any external change:

let map = use_map(state.env);
rsx! {
for (k, v) in map.entries.read().clone() {
div { "{k} = {v}" }
}
button { onclick: move |_| map.set_or_create("KEY".into(), "value".into()), "Add" }
button { onclick: move |_| map.remove("KEY".into()), "Remove" }
}

Subscribes to a single key in a ReactiveMap:

let entry = use_map_entry(state.env, "HTTP_PROXY".to_string());
rsx! { p { "{entry:?}" } }

For Tauri apps with a Dioxus WASM frontend, the provider and setup differ. The backend is a TauriBackend instead of a local store, and slices are loaded asynchronously before the app renders.

Use preload_slices! to declare which slices to load:

#[component]
fn App() -> Element {
let backend = TauriBackend::new();
rsx! {
AmeStateProvider {
backend,
init: preload_slices!(AppSettings, Theme),
Settings {}
}
}
}

preload_slices! suspends rendering until all slices are loaded from the Tauri backend. After that, use_amethystate::<S>() works the same as in the native case.