Skip to main content

yew_hooks/hooks/
use_cookie.rs

1use std::ops::Deref;
2use std::rc::Rc;
3
4use gloo::utils::document;
5use serde::{Deserialize, Serialize};
6use wasm_bindgen::JsValue;
7use yew::prelude::*;
8
9/// State handle for the [`use_cookie`] hook.
10pub struct UseCookieHandle<T> {
11    inner: UseStateHandle<Option<T>>,
12    key: Rc<String>,
13}
14
15impl<T> UseCookieHandle<T> {
16    /// Set a `value` for the specified key.
17    pub fn set(&self, value: T)
18    where
19        T: Serialize + Clone,
20    {
21        if let Ok(cookie_str) = serde_json::to_string(&value) {
22            // URL encode the value
23            let encoded_value = urlencoding::encode(&cookie_str);
24            let cookie = format!("{}={}", self.key, encoded_value);
25            set_cookie(&cookie);
26            self.inner.set(Some(value));
27        }
28    }
29
30    /// Set a `value` for the specified key with additional cookie attributes.
31    pub fn set_with_attributes(&self, value: T, attributes: CookieAttributes)
32    where
33        T: Serialize + Clone,
34    {
35        if let Ok(cookie_str) = serde_json::to_string(&value) {
36            // URL encode the value
37            let encoded_value = urlencoding::encode(&cookie_str);
38            let mut cookie = format!("{}={}", self.key, encoded_value);
39
40            if let Some(max_age) = attributes.max_age {
41                cookie.push_str(&format!("; max-age={}", max_age));
42            }
43            if let Some(path) = &attributes.path {
44                cookie.push_str(&format!("; path={}", path));
45            }
46            if let Some(domain) = &attributes.domain {
47                cookie.push_str(&format!("; domain={}", domain));
48            }
49            if attributes.secure {
50                cookie.push_str("; secure");
51            }
52            // Note: HttpOnly cookies cannot be set or accessed via JavaScript
53            // They can only be set server-side via HTTP headers
54            if let Some(same_site) = &attributes.same_site {
55                cookie.push_str(&format!("; SameSite={}", same_site));
56            }
57
58            set_cookie(&cookie);
59            self.inner.set(Some(value));
60        }
61    }
62
63    /// Delete a key and its stored value.
64    pub fn delete(&self) {
65        // Set expiration date in the past to delete the cookie
66        let cookie = format!("{}=; expires=Thu, 01 Jan 1970 00:00:00 GMT", self.key);
67        set_cookie(&cookie);
68        self.inner.set(None);
69    }
70}
71
72impl<T> Deref for UseCookieHandle<T> {
73    type Target = Option<T>;
74
75    fn deref(&self) -> &Self::Target {
76        &self.inner
77    }
78}
79
80impl<T> Clone for UseCookieHandle<T> {
81    fn clone(&self) -> Self {
82        Self {
83            inner: self.inner.clone(),
84            key: self.key.clone(),
85        }
86    }
87}
88
89impl<T> PartialEq for UseCookieHandle<T>
90where
91    T: PartialEq,
92{
93    fn eq(&self, other: &Self) -> bool {
94        *self.inner == *other.inner
95    }
96}
97
98/// Attributes for setting cookies
99#[derive(Debug, Clone, Default)]
100pub struct CookieAttributes {
101    /// Maximum age of the cookie in seconds
102    pub max_age: Option<i64>,
103    /// Path for which the cookie is valid
104    pub path: Option<String>,
105    /// Domain for which the cookie is valid
106    pub domain: Option<String>,
107    /// If true, the cookie is only sent over HTTPS
108    pub secure: bool,
109    /// Note: HttpOnly cannot be set via JavaScript - it's only for server-side cookies
110    /// This field is kept for API consistency but has no effect
111    pub http_only: bool,
112    /// SameSite attribute for the cookie
113    pub same_site: Option<SameSite>,
114}
115
116/// SameSite attribute values
117#[derive(Debug, Clone)]
118pub enum SameSite {
119    /// Strict same-site policy
120    Strict,
121    /// Lax same-site policy
122    Lax,
123    /// No same-site restriction
124    None,
125}
126
127impl std::fmt::Display for SameSite {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        match self {
130            SameSite::Strict => write!(f, "Strict"),
131            SameSite::Lax => write!(f, "Lax"),
132            SameSite::None => write!(f, "None"),
133        }
134    }
135}
136
137/// Set a cookie using JavaScript's document.cookie
138fn set_cookie(cookie: &str) {
139    let doc = document();
140    let _ = js_sys::Reflect::set(
141        &doc,
142        &JsValue::from_str("cookie"),
143        &JsValue::from_str(cookie),
144    );
145}
146
147/// Get the cookie string from document.cookie
148fn get_cookie_string() -> Option<String> {
149    let doc = document();
150    js_sys::Reflect::get(&doc, &JsValue::from_str("cookie"))
151        .ok()
152        .and_then(|v| v.as_string())
153}
154
155/// Parse a cookie string to get the value for a specific key
156fn get_cookie_value(key: &str) -> Option<String> {
157    get_cookie_string().and_then(|cookie_str| {
158        cookie_str
159            .split(';')
160            .map(|cookie| cookie.trim())
161            .find(|cookie| cookie.starts_with(&format!("{}=", key)))
162            .and_then(|cookie| cookie.split('=').nth(1))
163            .map(|value| value.to_string())
164    })
165}
166
167/// A side-effect hook that manages a single cookie.
168///
169/// # Example
170///
171/// ```rust
172/// # use yew::prelude::*;
173/// #
174/// use yew_hooks::prelude::*;
175///
176/// #[function_component(Cookie)]
177/// fn cookie() -> Html {
178///     let cookie = use_cookie::<String>("foo".to_string());
179///
180///     let onclick = {
181///         let cookie = cookie.clone();
182///         Callback::from(move |_| cookie.set("bar".to_string()))
183///     };
184///     let ondelete = {
185///         let cookie = cookie.clone();
186///         Callback::from(move |_| cookie.delete())
187///     };
188///
189///     html! {
190///         <div>
191///             <button onclick={onclick}>{ "Set to bar" }</button>
192///             <button onclick={ondelete}>{ "Delete" }</button>
193///             <p>
194///                 <b>{ "Current value: " }</b>
195///                 {
196///                     if let Some(value) = &*cookie {
197///                         html! { value }
198///                     } else {
199///                         html! {}
200///                     }
201///                 }
202///             </p>
203///         </div>
204///     }
205/// }
206/// ```
207#[hook]
208pub fn use_cookie<T>(key: String) -> UseCookieHandle<T>
209where
210    T: for<'de> Deserialize<'de> + 'static,
211{
212    let inner: UseStateHandle<Option<T>> = use_state(|| {
213        get_cookie_value(&key).and_then(|value| {
214            // URL decode the value
215            let decoded_value = urlencoding::decode(&value).ok()?;
216            serde_json::from_str(&decoded_value).ok()
217        })
218    });
219    let key = use_memo((), |_| key);
220
221    UseCookieHandle { inner, key }
222}