Skip to main content

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}