T
TanStack3y ago
eastern-cyan

Isomorphic queries with useSuspenseQuery

This is borderline a Next question, but you all would probably know just as well if not better. Streaming works incredibly well with ReactQueryStreamedHydration and useSuspenseQuery. That said, the isomorphic queries run by useSuspenseQuery raises an interesting problem. Any production system will need some sort of authentication token sent with the data requests. With fetches from the browser, those headers / cookies will be sent along automatically (assuming you have credentials: include). But for fetches coming from the server, Next will require you to add whatever headers you need, presumably the cookie at the very least. How do you all recommend this be managed? next/headers cannot be imported into client bundles. One solution I can imagine would be to grab the current headers, put them into context, and read that context inside of whatever (client) component you have useSuspenseQuery in. Detect if you're in a browser, and if so, just set credentials: include. Or if you're on the server (ie initial SSR on the client component), use the server headers you just put onto context. Is there an easier way I'm missing?
8 Replies
xenial-black
xenial-black3y ago
there's still the option to have your queries go against an /api route that you can hit from either the browser or the server, then have that api route call whatever external service you want. That's a bit of an overhead for SSR where you do ssr -> fetch(api route) -> fetch(real thing) instead of ssr -> fetch(real thing) but I think its an okay trade-off.
ratty-blush
ratty-blush3y ago
Hmm I’m actually literally calling an /api route just like you said. BUT the fetch TO that api route needs to have those headers attached, doesn’t it? It’s attaching those headers that I’m wondering about. When calling from a browser the fetch does it for me. But when calling from the server ….
xenial-black
xenial-black3y ago
Can you show an example sandbox of how you're currently doing it?
ratty-blush
ratty-blush3y ago
Sandbox would be tricky, but basically, in my root layout, I pass the relevant headers to my big Providers client component which renders a bunch of contexts (every project has one - the one that sets up react-query QueryClient context, etc). This sticks those headers onto context. The hook that has useSuspenseQuery (which will always be a client component unless I’m missing something) calls fetch, and does credentials: include if on the client, or picks up the headers from context if on the server. It’s in a client component so I can access context on the server. It works, I’m just wondering if ya’ll have a better way. The root problem afaict is that next/headers cannot be imported into a client component, so passing from a server component to a client component will always be necessary (iiuc) and this was the best I could come up with.
exotic-emerald
exotic-emerald3y ago
Sorry to flood your thread, but I just came across the same issue with useSuspenseQuery and thought I could provide some additional info. I have an API route handler that checks the user's auth session via NextAuth's getServerSession. If I fetch this API on the client, the headers are passed automatically and the session is validated in the route handler, however if it's called server-side, I need to manually pass the headers using next/headers to ensure the server receives the headers, like so:
import { headers } from 'next/headers';

...

export const getPages = async () => {
const res = await fetch(`${apiUrl}/pages`, {
next: { tags: ['pages'] },
method: 'GET',
headers: headers(),
});

if (!res.ok) throw new Error('Failed to fetch pages');

return res.json();
};
import { headers } from 'next/headers';

...

export const getPages = async () => {
const res = await fetch(`${apiUrl}/pages`, {
next: { tags: ['pages'] },
method: 'GET',
headers: headers(),
});

if (!res.ok) throw new Error('Failed to fetch pages');

return res.json();
};
If I import this getPages function into a Client Component, Next.js will throw an error since next/headers can only be imported into a Server Component. Below is what my Client Component looks like:
'use client'

export default function PagesNavItems() {
const { data } = useSuspenseQuery<Page[]>({
queryKey: ['pages'],
queryFn: getPages,
staleTime: 5 * 1000,
});

return (
<div>
{data?.map((page) => (
<Link>
...
</Link>
))}
</div>
);
}
'use client'

export default function PagesNavItems() {
const { data } = useSuspenseQuery<Page[]>({
queryKey: ['pages'],
queryFn: getPages,
staleTime: 5 * 1000,
});

return (
<div>
{data?.map((page) => (
<Link>
...
</Link>
))}
</div>
);
}
When I remove the headers: headers() line in the getPages function, the useSuspenseQuery will eventually receive the data, but it will fail in its first attempt to fetch the data (since no headers have been forwarded), and thus 403, but then the second attempt succeeds (presumably because it's made client-side? not sure). Here's my API route handler:
export async function GET() {
try {
const session = await getServerSession(authOptions);

if (!session) return new Response('Unauthorized', { status: 403 });

const data = await db.select().from(pages);

return new Response(JSON.stringify(data));
} catch (error) {

return new Response(null, { status: 500 });
}
}
export async function GET() {
try {
const session = await getServerSession(authOptions);

if (!session) return new Response('Unauthorized', { status: 403 });

const data = await db.select().from(pages);

return new Response(JSON.stringify(data));
} catch (error) {

return new Response(null, { status: 500 });
}
}
ratty-blush
ratty-blush3y ago
Did you try my solution above??
xenial-black
xenial-black3y ago
thanks for the detailed explanation. I don't really know a better way to make this work right now.
noble-gold
noble-gold3y ago
Hey! I was about to post a similar issue of using useSuspenseQuery in a client component that hits one of my nextjs api endpoints and getting a 401 since there is no auth in that scenario. Did this end up being an elegant solution for you? Probably going to go with waterfalling the data from a server component if not.

Did you find this page helpful?