Synphonyte / leptos-use

Collection of essential Leptos utilities inspired by React-Use / VueUse / SolidJS-USE
https://leptos-use.rs/
Apache License 2.0
309 stars 67 forks source link

`use_local_storage` doesn't work with `Option` or `Vec` #108

Closed NotAFlyingGoose closed 3 months ago

NotAFlyingGoose commented 4 months ago

In one of my projects I wanted to use local storage in order to store a list, but found a lot of hydration errors and an empty page on reload.

I minimized the issue to this:

/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
    view! {
        <a href="/subpage">"Go Away"</a>
    }
}

#[component]
fn SubPage() -> impl IntoView {
    let (thing, set_thing, _) = use_local_storage::<Option<String>, JsonCodec>("idk_something");

    view! {
        <button on:click=move |_| {
            set_thing.update(|list| *list = Some("hello".to_string()));
        }>
            "Set some"
        </button>

        {move || thing.get().map(|thing| view! {
            <p>{thing}</p>
        })}
    }
}

(made in leptos axum template)

When the sub page gets fully reloaded, there are hydration errors in the console (e.g. element with id 0-0-0-16 not found, ignoring it for hydration) and the "hello" message doesn't appear even if the value exists.

I used Option<String> here but it's the same thing if you use Vec<_>.

However, if you start at the home page and click the link to go to the subpage (using the site like a SPA), everything works as expected. This obviously falls under the Hydration Bugs chapter in the leptos book, and I'm wondering if there's a way to fix this on my end or if it's an issue in leptos-use.

Thanks! 😄

maccesch commented 4 months ago

Can you please provide your Config.toml for this project?

NotAFlyingGoose commented 3 months ago

It's basically the default Cargo.toml from the leptos axum starter template (and almost everything else in the project is from that template) + leptos-use. The only thing I really changed was the app.rs file with the components I gave above.

[package]
name = "leptos-weird-ssr"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos = { version = "0.6", features = ["nightly"] }
leptos-use = { version = "0.10.10", features = ["serde"] }
leptos_axum = { version = "0.6", optional = true }
leptos_meta = { version = "0.6", features = ["nightly"] }
leptos_router = { version = "0.6", features = ["nightly"] }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.92"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
gloo-timers = "0.3.0"

[features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
    "dep:axum",
    "dep:tokio",
    "dep:tower",
    "dep:tower-http",
    "dep:leptos_axum",
    "leptos/ssr",
    "leptos-use/ssr",
    "leptos_meta/ssr",
    "leptos_router/ssr",
    "dep:tracing",
]
default = ["ssr"]

# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"

[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos-weird-ssr"

# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"

# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"

# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"

# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3050"

# The port to use for automatic reload monitoring
reload-port = 3051

# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
#   [Windows] for non-WSL use "npx.cmd playwright test"
#   This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"

#  The browserlist query used for optimizing the CSS.
browserquery = "defaults"

# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"

# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]

# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false

# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]

# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

Also in case you ask, this is what the routes look like

<Routes>
    <Route path="" view=HomePage/>
    <Route path="/subpage" view=SubPage/>
</Routes>
maccesch commented 3 months ago

I think I finally know what is the issue here. It's the exact same issue as discussed here: https://discord.com/channels/1031524867910148188/1121154537709895783/1247572410350174278

So the recommendation is to use use_cookie instead.

I'm going to fix the hydration bugs but a flicker on reload will be unavoidable with local storage.

NotAFlyingGoose commented 3 months ago

Thanks! I'll put the discord messages here in case someone isn't in the leptos discord


maccesch — Today at 11:27 AM I think I finally get the issue here. The problem is that use_local_storage returns different values on the server than on the client because on the server there is no local storage obviously. So if you make a part of the DOM dependent on that value you will get hydration errors because now the DOM is different between the client and the server.

I think in this case the better option would be to just use use_cookie. That might be the better option anyway, even if there weren't any hydration errors, because you will still get flashing when loading one version of your page from the server which gets then changed quickly after the value from local storage has been read.

gbj I'd like your thoughts on this. If the user still wanted to use local storage for whatever reason what would be the best solution? Wrap it in use_effect? Is there sth that leptos-use can do to avoid these kinds of hydration errors?


gbj — Today at 11:42 AM I agree with your recommendation to use cookies for durable client-server storage rather than localStorage, for this reason (that the server doesn't have access to localStorage)

To handle hydration with localStorage, the strategy should basically be: in CSR mode (no SSR + hydration), the value of the localStorage can be either None (key not found in storage) or Some(T) (found in storage) when it initially runs in SSR mode, the value of localStorage is always None in hydrate mode, the value of localStore is always None initially, and then set with requestAnimationFrame or create_effect to whatever the actual value is (same as CSR value), so that it is always None during hydration and then set to the real value

This will always have some flicker but is the only way to hydrate it correctly


etc. they talk about how to fix it