solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
31.64k stars 887 forks source link

[SSR/Astro] DOM Not Updated from State by Client Updates onMount #2154

Closed NexRX closed 1 month ago

NexRX commented 1 month ago

Describe the bug

When SSRing with state and that state is updated on the client immediately/onMount, the new value is not reflected in the DOM.

Your Example Website or App

https://stackblitz.com/~/github.com/NexRX/astro-solid-nanostore

Steps to Reproduce the Bug or Issue

Using my example which demos a simple theme toggle defaulting to 'dark' mode...

  1. Click the toggle to change the state to 'light' mode.
  2. Reload the page
  3. Observe page reflects 'light' mode (because store subscriber is changing this) but controls show the 'dark' mode. a. Press the button to console log state which shows store actually holds 'light'

Expected behavior

When the client takes over after SSR the DOM reflect the updated state from onMount

Screenshots or Videos

Issue Demo Gif

Platform

Additional context

NexRX commented 1 month ago

Hi all, this has been sitting here for a bit now. Anything I can do to contribute to fixing this? :)

ryansolid commented 1 month ago

Well trying to decide what to do here. Solid is acting exactly how I'd expect it to. But I can see why the situation is awkward. Server renders dark, it hydrates thinking light, it assumes it matches which it doesn't and we don't do corrections. Then when you toggle it to light.. the client already thinks it is light so it doesn't propagate any change.. You have to change it to dark to change it to light again.

Your best bet is to have the server and client always match on initial render and then have the localStorage kick in. Which means it needs to hydrate dark. Which means nano-stores persistent atom isn't helping you here.

Changing your component to this works:

const nanoTheme = useStore($theme);
const [theme, setTheme] = createSignal("dark")
createEffect(() => {
   setTheme(nanoTheme())
})

return (<li class="link-card">
        <h2>
            Active Theme
        </h2>
        <p>
            {theme()} <br />
            Dark?: 
            <input type="checkbox" checked={theme() === "dark"} onChange={(e) => {
                $theme.set(e.target.checked ? "dark" : "light")
            }} />
            <br />
            <button onClick={() => console.log(`Atom value = '${$theme.get()}' | Store value = '${theme()}'`)}>Log Theme State</button>
        </p>
    </li>

But it does feel a bit like it isn't leveraging the the niceness of what nanostores is trying to offer. That being said persistent store as a source of truth with hydration seems like it isn't a good pattern in general.

ryansolid commented 1 month ago

Yeah the more I look at this the more I can't say this is a bug. Persistent storage is always going to cause rendering to not match unless you apply it after mount/hydration.

NexRX commented 1 month ago

Ah okay, thanks for the insight at least. What would be your goto for applying after mount? I tried using createEffect/onMount but had no luck. I assume you mean post the point of a onMount triggering and running?

ryansolid commented 1 month ago

No I meant onMount or createEffect. I edited the example and it seemed to switch fine. The problem is that the persistent nanostores start in the "correct" state which is wrong for hydration. So the server rendering and the client have to start at the fixed value and then apply the localStorage after. Typically in Solid I'd just be doing this by directly grabbing from localStorage in a onMount but given that the desire in your example was for nanostores to stay in sync I did that sort of createEffect syncing thing. Since I can't imagine this toggle is a hyper performance area the fact it writes in an effect should be fine.