mirror of
https://github.com/adoyle0/leptos-use.git
synced 2025-01-22 16:49:22 -05:00
parent
1b3889376b
commit
1771d3ac07
5 changed files with 126 additions and 50 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -3,6 +3,20 @@
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changes 🔥
|
||||
|
||||
- `ElementMaybeSignal` is now implemented for `websys::HtmlElement` (thanks to @blorbb).
|
||||
- `UseStorageOptions` now has `delay_during_hydration` which has to be used when you conditionally show parts of
|
||||
the DOM controlled by a value from storage. This leads to hydration errors which can be fixed by setting this new
|
||||
option to `true`.
|
||||
|
||||
### Breaking Changes 🛠
|
||||
|
||||
- `UseStorageOptions` no longer accepts a `codec` value because this is already provided as a generic parameter to
|
||||
the respective function calls.
|
||||
|
||||
## [0.10.10] - 2024-05-10
|
||||
|
||||
### Change 🔥
|
||||
|
|
|
@ -3,7 +3,7 @@ use leptos::ev::{keypress, KeyboardEvent};
|
|||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_use::storage::use_local_storage;
|
||||
use leptos_use::storage::{use_local_storage, use_local_storage_with_options, UseStorageOptions};
|
||||
use leptos_use::utils::FromToStringCodec;
|
||||
use leptos_use::{
|
||||
use_color_mode_with_options, use_cookie_with_options, use_debounce_fn, use_event_listener,
|
||||
|
@ -40,7 +40,10 @@ pub fn App() -> impl IntoView {
|
|||
#[component]
|
||||
fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
let (count, set_count, _) = use_local_storage::<i32, FromToStringCodec>("count-state");
|
||||
let (count, set_count, _) = use_local_storage_with_options::<i32, FromToStringCodec>(
|
||||
"count-state",
|
||||
UseStorageOptions::default().delay_during_hydration(true),
|
||||
);
|
||||
let on_click = move |_| set_count.update(|count| *count += 1);
|
||||
|
||||
let nf = use_intl_number_format(
|
||||
|
@ -96,6 +99,10 @@ fn HomePage() -> impl IntoView {
|
|||
<p>Dark preferred: {is_dark_preferred}</p>
|
||||
<LocalStorageTest/>
|
||||
<p>Test cookie: {move || test_cookie().unwrap_or("<Expired>".to_string())}</p>
|
||||
|
||||
<Show when={move || count() > 0 }>
|
||||
<div>Greater than 0 </div>
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,21 +17,21 @@ where
|
|||
T: Clone + Default + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(
|
||||
use_storage_with_options::<T, C>(
|
||||
StorageType::Local,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
UseStorageOptions::<T, C::Error>::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Accepts [`UseStorageOptions`]. See [`use_local_storage`] for details.
|
||||
pub fn use_local_storage_with_options<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
options: UseStorageOptions<T, C::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(StorageType::Local, key, options)
|
||||
use_storage_with_options::<T, C>(StorageType::Local, key, options)
|
||||
}
|
||||
|
|
|
@ -17,21 +17,21 @@ where
|
|||
T: Clone + Default + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(
|
||||
use_storage_with_options::<T, C>(
|
||||
StorageType::Session,
|
||||
key,
|
||||
UseStorageOptions::<T, C>::default(),
|
||||
UseStorageOptions::<T, C::Error>::default(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Accepts [`UseStorageOptions`]. See [`use_session_storage`] for details.
|
||||
pub fn use_session_storage_with_options<T, C>(
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
options: UseStorageOptions<T, C::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
use_storage_with_options(StorageType::Session, key, options)
|
||||
use_storage_with_options::<T, C>(StorageType::Session, key, options)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ use crate::{
|
|||
core::{MaybeRwSignal, StorageType},
|
||||
utils::{FilterOptions, StringCodec},
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use default_struct_builder::DefaultBuilder;
|
||||
use leptos::leptos_dom::HydrationCtx;
|
||||
use leptos::*;
|
||||
use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
|
@ -39,6 +40,7 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// # use serde::{Deserialize, Serialize};
|
||||
/// # use leptos_use::utils::{FromToStringCodec, JsonCodec, ProstCodec};
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Demo() -> impl IntoView {
|
||||
/// // Binds a struct:
|
||||
/// let (state, set_state, _) = use_local_storage::<MyState, JsonCodec>("my-state");
|
||||
|
@ -86,6 +88,64 @@ const INTERNAL_STORAGE_EVENT: &str = "leptos-use-storage";
|
|||
/// ## Server-Side Rendering
|
||||
///
|
||||
/// On the server the returned signals will just read/manipulate the `initial_value` without persistence.
|
||||
///
|
||||
/// ### Hydration bugs and `use_cookie`
|
||||
///
|
||||
/// If you use a value from storage to control conditional rendering you might run into issues with
|
||||
/// hydration.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::use_local_storage;
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
/// let (flag, set_flag, _) = use_session_storage::<bool, FromToStringCodec>("my-flag");
|
||||
///
|
||||
/// view! {
|
||||
/// <Show when=move || flag()>
|
||||
/// <div>Some conditional content</div>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can see hydration warnings in the browser console and the conditional parts of
|
||||
/// the app might never show up when rendered on the server and then hydrated in the browser. The
|
||||
/// reason for this is that the server has no access to storage and therefore will always use
|
||||
/// `initial_value` as described above. So on the server your app is always rendered as if
|
||||
/// the value from storage was `initial_value`. Then in the browser the actual stored value is used
|
||||
/// which might be different, hence during hydration the DOM looks different from the one rendered
|
||||
/// on the server which produces the hydration warnings.
|
||||
///
|
||||
/// The recommended way to avoid this is to use `use_cookie` instead because values stored in cookies
|
||||
/// are available on the server as well as in the browser.
|
||||
///
|
||||
/// If you still want to use storage instead of cookies you can use the `delay_during_hydration`
|
||||
/// option that will use the `initial_value` during hydration just as on the server and delay loading
|
||||
/// the value from storage by an animation frame. This gets rid of the hydration warnings and makes
|
||||
/// the app correctly render things. Some flickering might be unavoidable though.
|
||||
///
|
||||
/// ```
|
||||
/// # use leptos::*;
|
||||
/// # use leptos_use::storage::{use_local_storage_with_options, UseStorageOptions};
|
||||
/// # use leptos_use::utils::FromToStringCodec;
|
||||
/// #
|
||||
/// # #[component]
|
||||
/// # pub fn Example() -> impl IntoView {
|
||||
/// let (flag, set_flag, _) = use_session_storage_with_options::<bool, FromToStringCodec>(
|
||||
/// "my-flag",
|
||||
/// UseStorageOptions::default().delay_during_hydration(true),
|
||||
/// );
|
||||
///
|
||||
/// view! {
|
||||
/// <Show when=move || flag()>
|
||||
/// <div>Some conditional content</div>
|
||||
/// </Show>
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn use_storage<T, C>(
|
||||
storage_type: StorageType,
|
||||
|
@ -102,24 +162,27 @@ where
|
|||
pub fn use_storage_with_options<T, C>(
|
||||
storage_type: StorageType,
|
||||
key: impl AsRef<str>,
|
||||
options: UseStorageOptions<T, C>,
|
||||
options: UseStorageOptions<T, C::Error>,
|
||||
) -> (Signal<T>, WriteSignal<T>, impl Fn() + Clone)
|
||||
where
|
||||
T: Clone + PartialEq,
|
||||
C: StringCodec<T> + Default,
|
||||
{
|
||||
let UseStorageOptions {
|
||||
codec,
|
||||
on_error,
|
||||
listen_to_storage_changes,
|
||||
initial_value,
|
||||
filter,
|
||||
delay_during_hydration,
|
||||
} = options;
|
||||
|
||||
let codec = C::default();
|
||||
|
||||
let (data, set_data) = initial_value.into_signal();
|
||||
let default = data.get_untracked();
|
||||
|
||||
cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
let _ = codec;
|
||||
let _ = on_error;
|
||||
let _ = listen_to_storage_changes;
|
||||
|
@ -128,13 +191,15 @@ where
|
|||
let _ = key;
|
||||
let _ = INTERNAL_STORAGE_EVENT;
|
||||
|
||||
|
||||
let remove = move || {
|
||||
set_data.set(default.clone());
|
||||
};
|
||||
|
||||
(data.into(), set_data, remove)
|
||||
} else {
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use crate::{use_event_listener, use_window, watch_with_options, WatchOptions};
|
||||
|
||||
// Get storage API
|
||||
|
@ -176,6 +241,7 @@ where
|
|||
let codec = codec.to_owned();
|
||||
let key = key.as_ref().to_owned();
|
||||
let on_error = on_error.to_owned();
|
||||
|
||||
move || {
|
||||
let fetched = storage
|
||||
.to_owned()
|
||||
|
@ -212,7 +278,11 @@ where
|
|||
};
|
||||
|
||||
// Fetch initial value
|
||||
fetch_from_storage();
|
||||
if delay_during_hydration && HydrationCtx::is_hydrating() {
|
||||
request_animation_frame(fetch_from_storage.clone());
|
||||
} else {
|
||||
fetch_from_storage();
|
||||
}
|
||||
|
||||
// Fires when storage needs to be fetched
|
||||
let notify = create_trigger();
|
||||
|
@ -306,7 +376,7 @@ where
|
|||
};
|
||||
|
||||
(data, set_data, remove)
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
/// Session handling errors returned by [`use_storage_with_options`].
|
||||
|
@ -329,17 +399,26 @@ pub enum UseStorageError<Err> {
|
|||
}
|
||||
|
||||
/// Options for use with [`use_local_storage_with_options`], [`use_session_storage_with_options`] and [`use_storage_with_options`].
|
||||
pub struct UseStorageOptions<T: 'static, C: StringCodec<T>> {
|
||||
// Translates to and from UTF-16 strings
|
||||
codec: C,
|
||||
#[derive(DefaultBuilder)]
|
||||
pub struct UseStorageOptions<T, Err>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
// Callback for when an error occurs
|
||||
on_error: Rc<dyn Fn(UseStorageError<C::Error>)>,
|
||||
#[builder(skip)]
|
||||
on_error: Rc<dyn Fn(UseStorageError<Err>)>,
|
||||
// Whether to continuously listen to changes from browser storage
|
||||
listen_to_storage_changes: bool,
|
||||
// Initial value to use when the storage key is not set
|
||||
#[builder(skip)]
|
||||
initial_value: MaybeRwSignal<T>,
|
||||
// Debounce or throttle the writing to storage whenever the value changes
|
||||
#[builder(into)]
|
||||
filter: FilterOptions,
|
||||
/// Delays the reading of the value from storage by one animation frame during hydration.
|
||||
/// This ensures that during hydration the value is the initial value just like it is on the server
|
||||
/// which helps prevent hydration errors. Defaults to `false`.
|
||||
delay_during_hydration: bool,
|
||||
}
|
||||
|
||||
/// Calls the on_error callback with the given error. Removes the error from the Result to avoid double error handling.
|
||||
|
@ -351,43 +430,27 @@ fn handle_error<T, Err>(
|
|||
result.map_err(|err| (on_error)(err))
|
||||
}
|
||||
|
||||
impl<T: Default, C: StringCodec<T> + Default> Default for UseStorageOptions<T, C> {
|
||||
impl<T: Default, Err> Default for UseStorageOptions<T, Err> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codec: C::default(),
|
||||
on_error: Rc::new(|_err| ()),
|
||||
listen_to_storage_changes: true,
|
||||
initial_value: MaybeRwSignal::default(),
|
||||
filter: FilterOptions::default(),
|
||||
delay_during_hydration: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default, C: StringCodec<T>> UseStorageOptions<T, C> {
|
||||
/// Sets the codec to use for encoding and decoding values to and from UTF-16 strings.
|
||||
pub fn codec(self, codec: impl Into<C>) -> Self {
|
||||
Self {
|
||||
codec: codec.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default, Err> UseStorageOptions<T, Err> {
|
||||
/// Optional callback whenever an error occurs.
|
||||
pub fn on_error(self, on_error: impl Fn(UseStorageError<C::Error>) + 'static) -> Self {
|
||||
pub fn on_error(self, on_error: impl Fn(UseStorageError<Err>) + 'static) -> Self {
|
||||
Self {
|
||||
on_error: Rc::new(on_error),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen to changes to this storage key from browser and page events. Defaults to true.
|
||||
pub fn listen_to_storage_changes(self, listen_to_storage_changes: bool) -> Self {
|
||||
Self {
|
||||
listen_to_storage_changes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Initial value to use when the storage key is not set. Note that this value is read once on creation of the storage hook and not updated again. Accepts a signal and defaults to `T::default()`.
|
||||
pub fn initial_value(self, initial: impl Into<MaybeRwSignal<T>>) -> Self {
|
||||
Self {
|
||||
|
@ -395,12 +458,4 @@ impl<T: Default, C: StringCodec<T>> UseStorageOptions<T, C> {
|
|||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Debounce or throttle the writing to storage whenever the value changes.
|
||||
pub fn filter(self, filter: impl Into<FilterOptions>) -> Self {
|
||||
Self {
|
||||
filter: filter.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue