Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component doesn't reliably respond to a signal after hydration #2160

Closed
fasiha opened this issue May 13, 2024 · 3 comments
Closed

Component doesn't reliably respond to a signal after hydration #2160

fasiha opened this issue May 13, 2024 · 3 comments

Comments

@fasiha
Copy link

fasiha commented May 13, 2024

Describe the bug

I have SSR enabled, and a module-scoped signal that is set onMount in one component. A second component that reads that signal doesn't reliably get notified when that signal is set.

Your Example Website or App

https://stackblitz.com/edit/solidjs-templates-rqx1ev?file=vite.config.ts,src%2Findex.tsx,index.html,src%2FAvatar.tsx,src%2FUser.tsx

Steps to Reproduce the Bug or Issue

  1. Go to https://stackblitz.com/edit/solidjs-templates-rqx1ev?file=vite.config.ts,src%2Findex.tsx,index.html,src%2FAvatar.tsx,src%2FUser.tsx (kindly put together by Astro dev @matthewp via Solid component doesn't reliably respond to signal withastro/astro#11017)
  2. Click "Login" button (this sets localStorage user to "ahmed")
  3. Refresh a few times

Expected behavior

After refresh, I expect both bold lines to say User is "ahmed" (responding to the user signal being set by the User component's onMount)

However, often one or both will say User is "", not reacting to the signal. Sometimes this works, other times it doesn't.

Screenshots or Videos

solid-rehydrate-behavior

Platform

  • OS: dev is macOS but bug tested in macOS and Windows
  • Browser: all browsers tested
  • Version
    "solid-js": "^1.7.6"
    "vite": "^4.3.9",
    "vite-plugin-solid": "^2.7.0"

Additional context

Hypothesis from withastro/astro#11017:

There appears to be a timing bug related to hydration and components that render asynchronously.

@ryansolid
Copy link
Member

I see the issue it is just a bit tricky. The we are pretty lax when it comes to hydration in that we assume the server is correct and we don't correct text mismatches. If someone interacts with the page then we cancel hydration/streaming and apply the new state. However, if things are still hydrating we continue to assume the server is correct. What is happening in the example here is that since the one place is hydrating it assumes it doesn't need to do work but the signal under it has changed. So while the signal is being tracked and would update on another change it doesn't try to write the DOM initially.

That being said in general lazy hydrating + shared state is just not a good idea. Because the client state can change and if it is shared hydration will break. Like consider a global counter that can be incremented and the lazy hydrated component shows/hides stuff based on whether the count is above 5. If anything changed the counter before it hydrated you'd have serious hydration errors. This error was less serious and wasn't detected, but it would cause hydration issues just the same in other solutions that were stricter.

@fasiha
Copy link
Author

fasiha commented May 14, 2024

@ryansolid ahh, understood, thanks for that cogent explanation! Adding a onMount(() => console.log("Mounting …", new Date().toISOString())) to both components reveals that the incorrect behavior happens only when the signal-reader component (AvatarSolid in the example) is hydrated after the signal-setter component (User): the newly-hydrated component never "heard" the signal being set.

If I can burden your generosity some more. Should I just abandon the hydration + shared state model? Is that just a Bad Idea™️?

Or does one just have to be careful about this pitfall by making the signal-writer (User) write the signal only after it's confirmed that all readers have been hydrated? A terrible workaround might be to just wait 100 ms after page load to write to the signal. Are there better ways to work around this or should I just give up?

Edit: another terrible approach is to use an object as the signal's contents, and then in every hydrated reader component's onMount, check if the signal's contents have been updated client-side and if so, reassign the signal. In the context of my example:

// in signals.tsx
export const [user, setUser] = createSignal<{
  user: string | undefined;
}>({ user: undefined }); // undefined = server-side

// in User.tsx, the signal writer component
export const User: Component = () => {
  onMount(() => {
    const savedUser = localStorage.getItem("user") ?? ""; // string = client-side
    if (savedUser) setUser({ user: savedUser });
  });
  // ...
}

// in AvatarSolid.tsx, the signal reader component
export const AvatarSolid: Component = () => {
  onMount(() => {
    if (user().user !== undefined) setUser((x) => ({ ...x })); // cause a rerender if this component is hydrating late
  });
  // ...
}

@katywings
Copy link
Contributor

katywings commented May 14, 2024

@fasiha Maybe instead of onMount you could use https://primitives.solidjs.community/package/lifecycle#ishydrated as a way to check if you are safe to change the signal 🤔

Edit: I guess isHydrated does only make a difference if your app is hydrated only once. If you do something like below the user is correctly rendered in the indirect case.

function bootstrap(el: any, indirect: boolean, renderId: string) {
  (indirect ? render : hydrate)(
    () => (
      <Suspense>
        <Avatar indirect={indirect} />
      </Suspense>
    ),
    el,
    {
      renderId,
    }
  );
}

@fasiha fasiha closed this as completed May 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants