S
SolidJS3mo ago
Eddie

Hydration error when using <Show> in SolidStart

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>
);
}
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?
9 Replies
peerreynders
peerreynders3mo ago
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>
);
}
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
Eddie
EddieOP3mo ago
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)?
peerreynders
peerreynders3mo ago
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.
Eddie
EddieOP3mo ago
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>
);
}
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.
Madaxen86
Madaxen863mo ago
Can you share the implementation of getUserQuery?
Eddie
EddieOP2mo ago
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");
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;
}
"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;
}
peerreynders
peerreynders2mo ago
Are you by any chance passing <Footer /> as a prop somewhere? See: https://discord.com/channels/722131463138705510/1238643925988937820 If so use () => <Footer /> instead.
Eddie
EddieOP2mo ago
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>
);
}
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>
);
}
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>
);
}
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.
peerreynders
peerreynders2mo ago
I guess I learned that everything async requires a suspense somewhere up the component tree.
I seem to remember that it was mentioned somewhere that you need supense boundaries for SSR to work if you have async data sources. What wasn't mentioned was how it would fail.

Did you find this page helpful?