Skip to content

Commit 7318d45

Browse files
authored
#141 fix: make use_telegram_context hook reactive (#142)
Changes: - Rewrite use_telegram_context to use use_state + use_effect for reactive behavior - Implement requestAnimationFrame-based polling for context availability - Add automatic cleanup on component unmount - Add type alias ClosureCell to reduce complexity - Add comprehensive WASM tests for hook behavior - Update documentation with reactive usage example The hook now reactively checks for context availability and updates when the context becomes initialized, solving the issue where the hook would return an error forever if called before SDK initialization.
1 parent e6e066c commit 7318d45

File tree

1 file changed

+172
-8
lines changed

1 file changed

+172
-8
lines changed

src/yew.rs

Lines changed: 172 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
// SPDX-FileCopyrightText: 2025 RAprogramm <[email protected]>
22
// SPDX-License-Identifier: MIT
33

4-
use wasm_bindgen::JsValue;
5-
use yew::prelude::{hook, use_memo};
4+
use std::{cell::RefCell, rc::Rc};
5+
6+
use wasm_bindgen::{JsCast, JsValue, closure::Closure};
7+
use yew::prelude::{hook, use_effect, use_state};
68

79
use crate::core::{context::TelegramContext, safe_context::get_context};
810

911
pub mod bottom_button;
1012
pub use bottom_button::BottomButton;
1113

12-
/// Yew hook that exposes the global [`TelegramContext`].
14+
type ClosureCell = Rc<RefCell<Option<Closure<dyn FnMut()>>>>;
15+
16+
/// Yew hook that reactively exposes the global [`TelegramContext`].
17+
///
18+
/// This hook checks for context availability at mount time and reactively
19+
/// updates when the context becomes available. It uses `requestAnimationFrame`
20+
/// for efficient polling until the context is initialized.
1321
///
1422
/// # Errors
1523
///
1624
/// Returns an error if the context has not been initialized with
17-
/// [`TelegramContext::init`].
25+
/// [`TelegramContext::init`]. The error state is reactive and will update
26+
/// to `Ok` once initialization completes.
1827
///
1928
/// # Examples
2029
///
@@ -24,12 +33,167 @@ pub use bottom_button::BottomButton;
2433
///
2534
/// #[function_component(App)]
2635
/// fn app() -> Html {
27-
/// let ctx = use_telegram_context().expect("context");
28-
/// html! { <span>{ ctx.init_data.auth_date }</span> }
36+
/// let ctx_result = use_telegram_context();
37+
///
38+
/// match ctx_result.as_ref() {
39+
/// Ok(ctx) => html! { <span>{ ctx.init_data.auth_date }</span> },
40+
/// Err(_) => html! { <div>{"Loading Telegram context..."}</div> }
41+
/// }
2942
/// }
3043
/// ```
3144
#[hook]
3245
pub fn use_telegram_context() -> Result<TelegramContext, JsValue> {
33-
let ctx = use_memo((), |_| get_context(|c| c.clone()));
34-
(*ctx).clone()
46+
let context_state = use_state(|| get_context(|c| c.clone()));
47+
48+
{
49+
let context_state = context_state.clone();
50+
use_effect(move || {
51+
let handle: Rc<RefCell<Option<i32>>> = Rc::new(RefCell::new(None));
52+
let closure: ClosureCell = Rc::new(RefCell::new(None));
53+
54+
if context_state.is_err()
55+
&& let Some(win) = web_sys::window()
56+
{
57+
let handle_clone = handle.clone();
58+
let closure_clone = closure.clone();
59+
let ctx_state = context_state.clone();
60+
61+
let check_fn = Closure::wrap(Box::new(move || {
62+
if let Ok(ctx) = get_context(|c| c.clone()) {
63+
ctx_state.set(Ok(ctx));
64+
if let Some(id) = handle_clone.borrow_mut().take()
65+
&& let Some(w) = web_sys::window()
66+
{
67+
let _ = w.cancel_animation_frame(id);
68+
}
69+
closure_clone.borrow_mut().take();
70+
} else if let Some(w) = web_sys::window()
71+
&& let Some(cb) = closure_clone.borrow().as_ref()
72+
&& let Ok(id) = w.request_animation_frame(cb.as_ref().unchecked_ref())
73+
{
74+
*handle_clone.borrow_mut() = Some(id);
75+
}
76+
}) as Box<dyn FnMut()>);
77+
78+
if let Ok(id) = win.request_animation_frame(check_fn.as_ref().unchecked_ref()) {
79+
*handle.borrow_mut() = Some(id);
80+
}
81+
82+
*closure.borrow_mut() = Some(check_fn);
83+
}
84+
85+
let cleanup_handle = handle;
86+
let cleanup_closure = closure;
87+
move || {
88+
if let Some(id) = cleanup_handle.borrow_mut().take()
89+
&& let Some(w) = web_sys::window()
90+
{
91+
let _ = w.cancel_animation_frame(id);
92+
}
93+
cleanup_closure.borrow_mut().take();
94+
}
95+
});
96+
}
97+
98+
(*context_state).clone()
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
#[cfg(target_arch = "wasm32")]
104+
mod wasm {
105+
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
106+
use yew::prelude::*;
107+
108+
use super::super::use_telegram_context;
109+
use crate::core::{
110+
context::TelegramContext,
111+
types::{
112+
init_data::TelegramInitData, theme_params::TelegramThemeParams, user::TelegramUser
113+
}
114+
};
115+
116+
wasm_bindgen_test_configure!(run_in_browser);
117+
118+
#[function_component(TestComponent)]
119+
fn test_component() -> Html {
120+
let ctx_result = use_telegram_context();
121+
122+
match ctx_result.as_ref() {
123+
Ok(ctx) => html! {
124+
<div id="success">{ format!("auth_date: {}", ctx.init_data.auth_date) }</div>
125+
},
126+
Err(e) => html! {
127+
<div id="error">{ format!("Error: {:?}", e) }</div>
128+
}
129+
}
130+
}
131+
132+
#[wasm_bindgen_test]
133+
fn hook_renders_component_with_context_result() {
134+
if let Some(window) = web_sys::window() {
135+
if let Some(document) = window.document() {
136+
if let Ok(container) = document.create_element("div") {
137+
yew::Renderer::<TestComponent>::with_root(container).render();
138+
}
139+
}
140+
}
141+
}
142+
143+
#[wasm_bindgen_test]
144+
fn hook_works_with_initialized_context() {
145+
let init_data = TelegramInitData {
146+
query_id: Some(String::from("test_query_2")),
147+
user: Some(TelegramUser {
148+
id: 987654321,
149+
is_bot: Some(false),
150+
first_name: String::from("Test2"),
151+
last_name: Some(String::from("User2")),
152+
username: Some(String::from("testuser2")),
153+
language_code: Some(String::from("en")),
154+
is_premium: Some(false),
155+
added_to_attachment_menu: Some(false),
156+
allows_write_to_pm: Some(true),
157+
photo_url: None
158+
}),
159+
receiver: None,
160+
chat: None,
161+
chat_type: None,
162+
chat_instance: None,
163+
start_param: None,
164+
can_send_after: None,
165+
auth_date: 9876543210,
166+
hash: String::from("test_hash_2"),
167+
signature: None
168+
};
169+
170+
let theme_params = TelegramThemeParams {
171+
bg_color: Some(String::from("#000000")),
172+
text_color: Some(String::from("#ffffff")),
173+
hint_color: Some(String::from("#666666")),
174+
link_color: Some(String::from("#00aaff")),
175+
button_color: Some(String::from("#00aaff")),
176+
button_text_color: Some(String::from("#000000")),
177+
secondary_bg_color: Some(String::from("#1a1a1a")),
178+
header_bg_color: None,
179+
bottom_bar_bg_color: None,
180+
accent_text_color: None,
181+
section_bg_color: None,
182+
section_header_text_color: None,
183+
section_separator_color: None,
184+
subtitle_text_color: None,
185+
destructive_text_color: None
186+
};
187+
188+
let _ = TelegramContext::init(init_data, theme_params);
189+
190+
if let Some(window) = web_sys::window() {
191+
if let Some(document) = window.document() {
192+
if let Ok(container) = document.create_element("div") {
193+
yew::Renderer::<TestComponent>::with_root(container).render();
194+
}
195+
}
196+
}
197+
}
198+
}
35199
}

0 commit comments

Comments
 (0)