27_keyed_state/27_keyed_state.rs
1//! Example 27: Keyed State (Order-Independent Hooks)
2//!
3//! This example demonstrates the new `state!` macro which provides
4//! order-independent state hooks. Unlike `use_state`, these can be used
5//! conditionally without causing panics.
6//!
7//! Run with: cargo run -p telex --example 27_keyed_state
8//!
9//! ## The Problem with use_state
10//!
11//! Traditional hooks must be called in the same order every render:
12//! ```text
13//! // WRONG - This panics!
14//! if show_counter {
15//! let count = cx.use_state(|| 0); // Index shifts based on condition
16//! }
17//! ```
18//!
19//! ## The Solution: state!
20//!
21//! Each macro invocation creates a unique type as the key, so order doesn't matter:
22//! ```text
23//! // SAFE - This works!
24//! if show_counter {
25//! let count = state!(cx, || 0); // Key is baked into the code location
26//! }
27//! ```
28
29use crossterm::event::KeyCode;
30use telex::prelude::*;
31use telex::Color;
32
33telex::require_api!(0, 2);
34
35fn main() {
36 telex::run_with_theme(App, telex::theme::Theme::nord()).unwrap();
37}
38
39struct App;
40
41impl Component for App {
42 fn render(&self, cx: Scope) -> View {
43 let show_help = state!(cx, || false);
44
45 // F1 toggles help
46 cx.use_command(
47 KeyBinding::key(KeyCode::F(1)),
48 with!(show_help => move || show_help.update(|v| *v = !*v)),
49 );
50
51 // Two independent toggles
52 let show_a = state!(cx, || true);
53 let show_b = state!(cx, || true);
54
55 // COUNTER A - state created inside conditional
56 let counter_a = if show_a.get() {
57 let count = state!(cx, || 0);
58 let inc = with!(count => move || count.update(|n| *n += 1));
59
60 View::hstack()
61 .spacing(1)
62 .child(
63 View::styled_text(format!("{}", count.get()))
64 .color(Color::Yellow)
65 .bold()
66 .build(),
67 )
68 .child(View::button().label("+").on_press(inc).build())
69 .build()
70 } else {
71 View::styled_text("--").dim().build()
72 };
73
74 // COUNTER B - state created inside a DIFFERENT conditional
75 let counter_b = if show_b.get() {
76 let count = state!(cx, || 0);
77 let inc = with!(count => move || count.update(|n| *n += 1));
78
79 View::hstack()
80 .spacing(1)
81 .child(
82 View::styled_text(format!("{}", count.get()))
83 .color(Color::Magenta)
84 .bold()
85 .build(),
86 )
87 .child(View::button().label("+").on_press(inc).build())
88 .build()
89 } else {
90 View::styled_text("--").dim().build()
91 };
92
93 let toggle_a = with!(show_a => move |_: bool| show_a.update(|b| *b = !*b));
94 let toggle_b = with!(show_b => move |_: bool| show_b.update(|b| *b = !*b));
95
96 View::vstack()
97 .spacing(1)
98 .child(
99 View::styled_text("state! Demo")
100 .color(Color::Cyan)
101 .bold()
102 .build(),
103 )
104 .child(View::gap(1))
105 .child(
106 View::hstack()
107 .spacing(2)
108 // Counter A box
109 .child(
110 View::boxed()
111 .border(true)
112 .padding(1)
113 .max_width(25)
114 .child(
115 View::vstack()
116 .child(View::styled_text("Counter A").bold().build())
117 .child(View::gap(1))
118 .child(
119 View::hstack()
120 .spacing(1)
121 .child(View::text("Value:"))
122 .child(counter_a)
123 .build(),
124 )
125 .child(
126 View::hstack()
127 .spacing(1)
128 .child(View::text("Show:"))
129 .child(
130 View::checkbox()
131 .checked(show_a.get())
132 .on_toggle(toggle_a)
133 .build(),
134 )
135 .build(),
136 )
137 .build(),
138 )
139 .build(),
140 )
141 // Counter B box
142 .child(
143 View::boxed()
144 .border(true)
145 .padding(1)
146 .max_width(25)
147 .child(
148 View::vstack()
149 .child(View::styled_text("Counter B").bold().build())
150 .child(View::gap(1))
151 .child(
152 View::hstack()
153 .spacing(1)
154 .child(View::text("Value:"))
155 .child(counter_b)
156 .build(),
157 )
158 .child(
159 View::hstack()
160 .spacing(1)
161 .child(View::text("Show:"))
162 .child(
163 View::checkbox()
164 .checked(show_b.get())
165 .on_toggle(toggle_b)
166 .build(),
167 )
168 .build(),
169 )
170 .build(),
171 )
172 .build(),
173 )
174 .build(),
175 )
176 .child(View::gap(1))
177 .child(View::styled_text("Try this:").bold().build())
178 .child(View::text(
179 " 1. Increment both counters to different values",
180 ))
181 .child(View::text(" 2. Hide counter A (uncheck its box)"))
182 .child(View::text(" 3. Counter B continues to work just fine!"))
183 .child(View::text(" 4. Show A again - it remembers its value"))
184 .child(View::gap(1))
185 .child(
186 View::styled_text("They don't interfere with each other.")
187 .color(Color::Green)
188 .build(),
189 )
190 .child(View::gap(1))
191 .child(
192 View::boxed()
193 .border(true)
194 .padding(1)
195 .child(
196 View::vstack()
197 .child(View::styled_text("The code:").bold().build())
198 .child(View::gap(1))
199 .child(
200 View::styled_text("if show_a.get() {")
201 .color(Color::DarkGrey)
202 .build(),
203 )
204 .child(
205 View::styled_text(" let count = state!(cx, || 0);")
206 .color(Color::Yellow)
207 .build(),
208 )
209 .child(View::styled_text("}").color(Color::DarkGrey).build())
210 .child(
211 View::styled_text("if show_b.get() {")
212 .color(Color::DarkGrey)
213 .build(),
214 )
215 .child(
216 View::styled_text(" let count = state!(cx, || 0);")
217 .color(Color::Magenta)
218 .build(),
219 )
220 .child(View::styled_text("}").color(Color::DarkGrey).build())
221 .child(View::gap(1))
222 .child(View::text("With use_state, hiding A would CRASH B"))
223 .child(View::text("(hook indices would shift)."))
224 .build(),
225 )
226 .build(),
227 )
228 .child(View::gap(1))
229 .child(
230 View::styled_text("Tab: navigate | F1 help | Ctrl+Q: quit")
231 .dim()
232 .build(),
233 )
234 .child(
235 View::modal()
236 .visible(show_help.get())
237 .title("Example 27: Keyed State")
238 .on_dismiss(with!(show_help => move || show_help.set(false)))
239 .child(
240 View::vstack()
241 .child(View::styled_text("What you're seeing").bold().build())
242 .child(View::text("• state! macro for order-independent hooks"))
243 .child(View::text("• Conditional state that doesn't crash"))
244 .child(View::text("• Two counters with hide/show toggles"))
245 .child(View::gap(1))
246 .child(View::styled_text("Key concepts").bold().build())
247 .child(View::text("• state!(cx, || init) creates keyed state"))
248 .child(View::text("• Each call site gets unique key"))
249 .child(View::text("• Safe to use inside if blocks"))
250 .child(View::text("• Values persist when hidden/shown"))
251 .child(View::gap(1))
252 .child(View::styled_text("Try this").bold().build())
253 .child(View::text("• Increment both counters"))
254 .child(View::text("• Hide counter A"))
255 .child(View::text("• Counter B still works!"))
256 .child(View::text("• Show A again - value preserved"))
257 .child(View::gap(1))
258 .child(View::styled_text("Next up").bold().build())
259 .child(View::text("→ 28_shared_state: shared state via keys"))
260 .child(View::gap(1))
261 .child(View::styled_text("Press Escape to close").dim().build())
262 .build(),
263 )
264 .build(),
265 )
266 .build()
267 }
268}