Shared State
Multiple components sharing the same state via explicit keys.
struct SharedCounterKey;
// Both get the SAME state
let count_a = cx.use_state_keyed::<SharedCounterKey, _>(|| 0);
let count_b = cx.use_state_keyed::<SharedCounterKey, _>(|| 0);
count_a.update(|n| *n += 1); // Both see the update!
Run with: cargo run -p telex-tui --example 28_shared_state
The Concept
In Keyed State, we saw that state! creates unique keys for each call site, giving you independent state. Shared state is the opposite: same key = same state.
By using use_state_keyed with an explicit key type, multiple parts of your code can access the same underlying value.
Defining a Key
Create a zero-sized type to use as a key:
struct MySharedKey;
That's it. The type name becomes the key. Any code that uses MySharedKey will share state.
Using the Key
Access shared state with use_state_keyed:
struct CounterKey;
fn render(&self, cx: Scope) -> View {
// First access - creates the state
let count = cx.use_state_keyed::<CounterKey, _>(|| 0);
// Later, elsewhere - gets the SAME state
let count2 = cx.use_state_keyed::<CounterKey, _>(|| 0);
// count and count2 point to the same value
count.get() == count2.get() // always true
}
The initializer || 0 only runs once, when the key is first encountered. Subsequent calls retrieve the existing state.
Multiple Components Sharing State
Different parts of your UI can share state:
struct SelectedTabKey;
fn tab_bar(cx: Scope) -> View {
let selected = cx.use_state_keyed::<SelectedTabKey, _>(|| 0);
// Render tab buttons that modify selected
}
fn tab_content(cx: Scope) -> View {
let selected = cx.use_state_keyed::<SelectedTabKey, _>(|| 0);
// Render content based on selected tab
}
// Both functions access the same selected tab index
When the tab bar changes the selection, the content updates automatically.
Shared State Across Renders
State persists across renders, so all access points see the same value:
struct AppModeKey;
fn render(&self, cx: Scope) -> View {
let mode = cx.use_state_keyed::<AppModeKey, _>(|| Mode::View);
View::vstack()
.child(toolbar(cx)) // reads mode
.child(main_content(cx)) // reads mode
.child(status_bar(cx)) // reads mode
.build()
}
fn toolbar(cx: Scope) -> View {
let mode = cx.use_state_keyed::<AppModeKey, _>(|| Mode::View);
// mode reflects current app mode set anywhere else
}
Real-World Example: Theme Switcher
A global theme setting:
struct ThemeKey;
fn app(cx: Scope) -> View {
View::vstack()
.child(header(cx))
.child(content(cx))
.child(settings_panel(cx))
.build()
}
fn header(cx: Scope) -> View {
let theme = cx.use_state_keyed::<ThemeKey, _>(|| "dark");
let bg_color = match theme.get().as_str() {
"dark" => Color::DarkGrey,
"light" => Color::White,
_ => Color::Blue,
};
View::boxed()
.background(bg_color)
.child(View::text("Header"))
.build()
}
fn settings_panel(cx: Scope) -> View {
let theme = cx.use_state_keyed::<ThemeKey, _>(|| "dark");
View::button()
.label("Toggle Theme")
.on_press(with!(theme => move || {
theme.update(|t| {
*t = if *t == "dark" { "light".to_string() } else { "dark".to_string() }
});
}))
.build()
}
Clicking the button in settings updates the theme everywhere.
Multiple Shared States
You can have as many shared states as you need:
struct UserNameKey;
struct UserEmailKey;
struct UserAvatarKey;
fn profile_editor(cx: Scope) -> View {
let name = cx.use_state_keyed::<UserNameKey, _>(String::new);
let email = cx.use_state_keyed::<UserEmailKey, _>(String::new);
let avatar = cx.use_state_keyed::<UserAvatarKey, _>(|| "default.png");
// Edit UI
}
fn profile_display(cx: Scope) -> View {
let name = cx.use_state_keyed::<UserNameKey, _>(String::new);
let email = cx.use_state_keyed::<UserEmailKey, _>(String::new);
let avatar = cx.use_state_keyed::<UserAvatarKey, _>(|| "default.png");
// Display UI - sees changes from editor
}
Each key is independent - changing name doesn't affect email.
Shared State in Loops
Shared keys work inside loops:
struct ListSelectionKey;
for item in items.get() {
let selected = cx.use_state_keyed::<ListSelectionKey, _>(|| 0);
// All iterations see the same selection
let is_selected = selected.get() == item.id;
}
This is useful for coordinating behavior across list items.
When to Use Shared State
Use shared state when:
- Multiple UI components need the same data
- You want global app-level state (theme, user session, etc.)
- Avoiding prop drilling through many layers
- Coordinating state across distant components
Use regular state when:
- State is local to one component
- You want isolation and independence
- State doesn't need to be accessed elsewhere
Shared State vs Context
Both solve similar problems - sharing data across components. The difference:
Shared State (use_state_keyed):
- Explicit key types
- Compile-time enforcement
- State is mutable via
State<T>API - Good for frequently changing data
Context (use_context):
- Provider/consumer pattern
- Runtime lookup (returns
Option<T>) - Data is typically immutable
- Good for configuration and DI
See Context for details.
Type Safety
Keys are type-safe. Different keys can't accidentally collide:
struct CountKey;
struct NameKey;
let count = cx.use_state_keyed::<CountKey, _>(|| 0i32);
let name = cx.use_state_keyed::<NameKey, _>(String::new);
// These are completely independent
// No runtime conflicts possible
Initialization
The initializer runs only once, when the key is first used:
struct ExpensiveKey;
let data = cx.use_state_keyed::<ExpensiveKey, _>(|| {
println!("Initializing!"); // Prints once
load_from_disk()
});
let data2 = cx.use_state_keyed::<ExpensiveKey, _>(|| {
println!("Initializing!"); // Never prints
load_from_disk() // Never runs
});
Sharing with Explicit Keys
You can parameterize keys for fine-grained sharing:
// Instead of a single shared counter:
struct CounterKey;
// Use multiple keys for independent counters:
struct CounterAKey;
struct CounterBKey;
struct CounterCKey;
let a = cx.use_state_keyed::<CounterAKey, _>(|| 0);
let b = cx.use_state_keyed::<CounterBKey, _>(|| 0);
let c = cx.use_state_keyed::<CounterCKey, _>(|| 0);
Or use a generic key with phantom data:
use std::marker::PhantomData;
struct CounterKey<T>(PhantomData<T>);
struct A;
struct B;
let counter_a = cx.use_state_keyed::<CounterKey<A>, _>(|| 0);
let counter_b = cx.use_state_keyed::<CounterKey<B>, _>(|| 0);
Common Patterns
Global loading state:
struct LoadingKey;
fn anywhere(cx: Scope) {
let loading = cx.use_state_keyed::<LoadingKey, _>(|| false);
loading.set(true); // Shows loading spinner globally
}
Sidebar open/closed:
struct SidebarOpenKey;
fn sidebar_toggle(cx: Scope) -> View {
let open = cx.use_state_keyed::<SidebarOpenKey, _>(|| true);
View::button()
.label(if open.get() { "Close" } else { "Open" })
.on_press(with!(open => move || open.update(|b| *b = !*b)))
.build()
}
fn sidebar(cx: Scope) -> View {
let open = cx.use_state_keyed::<SidebarOpenKey, _>(|| true);
if !open.get() {
return View::empty();
}
// Sidebar content
}
Selected item ID:
struct SelectedItemKey;
fn item_list(cx: Scope, items: Vec<Item>) -> View {
let selected = cx.use_state_keyed::<SelectedItemKey, _>(|| None::<usize>);
View::list()
.items(items.iter().map(|i| i.name.clone()).collect())
.selected(selected.get().unwrap_or(0))
.on_select(with!(selected => move |idx| selected.set(Some(idx))))
.build()
}
fn item_details(cx: Scope, items: Vec<Item>) -> View {
let selected = cx.use_state_keyed::<SelectedItemKey, _>(|| None::<usize>);
match selected.get() {
Some(idx) => View::text(&items[idx].description),
None => View::text("No item selected"),
}
}
Debugging
If you see unexpected sharing, check that your key types are distinct:
// BAD - Same key name in different modules
mod panel_a {
struct SelectedKey;
}
mod panel_b {
struct SelectedKey; // Different type, but if imported wrong...
}
// GOOD - Unique names
struct PanelASelectedKey;
struct PanelBSelectedKey;
Performance
Shared state has the same performance as regular state:
- O(1) key lookup (hash map)
- O(1) read/write via
State<T>
The only difference is the key is provided explicitly instead of generated.
Limitations
No scoping: Shared state is global to the component tree. You can't have "scoped" shared state that only exists in part of the tree.
No hierarchies: Keys are flat - you can't nest or inherit keys.
Manual coordination: You're responsible for ensuring the right components use the right keys.
For scoped sharing, use Context instead.
Tips
Name keys clearly - struct ThemeKey is better than struct Key1.
One key per concept - Don't reuse keys for unrelated data.
Document key purpose - Add comments explaining what each key is for:
/// Shared state for currently selected tab index
struct SelectedTabKey;
Prefer context for config - Use shared state for mutable data, context for immutable config.
Group related keys - Put keys in a module:
mod app_state {
pub struct ThemeKey;
pub struct UserKey;
pub struct LoadingKey;
}
Next: Context