#Hydration error when using <Show> in SolidStart

11 messages · Page 1 of 1 (latest)

worn isle
#

I am new to SolidStart, I see this talked about in several places but I fail to understand how any of the mentioned solutions applies to my case.

I am trying to conditionally render something using <Show>:

export function Footer() {
  const user = createAsync(() => getUserQuery({}));

  return (
    <footer>
      <Show when={user()}>
        <p>
          Logged in as: <strong>{user()?.name}</strong>
        </p>
      </Show>
    </footer>
  );
}

This results in a hydration error. I am fairly sure that I understand why I'm getting the error, but what pattern should I use to conditionally render an element like this?

real shell
#

Try:

export function Footer() {
  const user = createAsync(() => getUserQuery({}), { deferStream: true });

  return (
    <footer>
      <Show when={user()}>
        {(user) => (
          <p>
            Logged in as: <strong>{user().name}</strong>
          </p>
        )}
      </Show>
    </footer>
  );
}

deferStream

worn isle
#

That does not solve the issue, unfortunately.

#

In my case, the resource user is preloaded on the route that in turn renders this component, so I assume that when the Footer component function runs, it is already a fulfilled promise.

  • Does this mean that deferStream: true will have no effect here?
  • Why, if user() is fulfilled when this component mounts, is the when clause in the <Show> not making the content of the <Show> not render on mount (and in turn not cause hydration error because the element is there from the get-go)?
real shell
#

Hydration errors only happen after SSR, so the preload doesn't come into play. deferStream: true prevents the page from streaming until the promise accessed by createAsynchas settled, ensuring that the value is available for rendering.

The fact that the <Show /> isn't rendering suggests that the promise resolved to a falsy value.

One thing you should try is to view to page in incognito as some browser extensions change the DOM which can lead to hydration errors.

worn isle
#

I tried a fresh browser session, it did not help.

I have been banging my head against this for some time now and can't figure it out, I ended up doing it this way, which works:

export function Footer() {
  const user = createAsync(() => getUserQuery({}), { deferStream: true });

  const [hasUser, setHasUser] = createSignal(false);

  createEffect(() => {
    if (user()) {
      setHasUser(true);
    }
  });

  return (
    <footer>
      <Show when={hasUser()} fallback={<div>User not logged in..</div>}>
        {() => {
          return <div>Logged in</div>;
        }}
      </Show>
    </footer>
  );
}

If, in the above example, I replace hasUser() with user() in the when clause, I get the hydration error again.

I would have preferred to put the user() directly in the when clause, and I am still wondering if I should have been able to.

dire gyro
#

Can you share the implementation of getUserQuery?

worn isle
#

I got caught up in other things and just got back to this. Giving it another shot ❤️

import { query } from "@solidjs/router";
import { getUser } from "~/lib/serverFunctions";

export const getUserQuery = query(getUser, "userSession");
"use server";

import { redirect } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { auth } from "~/lib/auth";

export async function getUser(options: {
  redirectToIfExists?: string;
  redirectToIfNotExists?: string;
}) {
  const { redirectToIfExists, redirectToIfNotExists } = options;

  const event = getRequestEvent();
  const headers = event?.request.headers;

  if (!headers) {
    throw new Error("Headers not available");
  }

  const userSession = await auth.api.getSession({ headers });

  if (userSession?.user && redirectToIfExists) {
    console.log(`--> Found user; Redirecting: ${redirectToIfExists}`);
    throw redirect(redirectToIfExists);
  }

  if (!userSession?.user && redirectToIfNotExists) {
    console.log(`--> No user found; Redirecting: ${redirectToIfNotExists}`);
    throw redirect(redirectToIfNotExists);
  }

  if (!userSession || !userSession.user) {
    return null;
  }

  return userSession.user;
}
real shell
worn isle
#

No I believe not, I just use it like this.

export default function App() {
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>Tiiitle</Title>
          <div class="min-h-screen flex flex-col">
            <Suspense>{props.children}</Suspense>
            <div class="mt-auto">
              <Footer />
            </div>
          </div>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}
worn isle
#

Wrapping the <Show> in a <Suspense> seems to fix the issue for me. That led me to adding the suspense tag in the app.tsx file:

export default function App() {
  return (
    <Router
      root={(props) => (
        <MetaProvider>
          <Title>Tiiitle</Title>
          <div class="min-h-screen flex flex-col">
            <Suspense>{props.children}</Suspense>
            <div class="mt-auto">
              <Suspense>
                <Footer />
              </Suspense>
            </div>
          </div>
        </MetaProvider>
      )}
    >
      <FileRoutes />
    </Router>
  );
}

And I don't longer need to have the suspense in the footer component. So, I guess I learned that everything async requires a suspense somewhere up the component tree.