Server-side fetch and client-side only filtering
Unanswered
Blue orchard bee posted this in #help-forum
Blue orchard beeOP
Using the app router I fetch my data server-side and render my list of items through server components with relative ease
The workflow I'm after is to first serve the full list of results and have it available from the first render generated server-side. This has 2 benefits to me
- The complete list of items is indexable
- No CLS issue because the full list is rendered directly
I have that list of items filtered against values stored in the query parameters to manage the state in order to have shareable URLs
The issue now becomes that the component calling
This feels like fixed probleme already, are there recipes with that kind of workflow? I must be missing something obvious
The workflow I'm after is to first serve the full list of results and have it available from the first render generated server-side. This has 2 benefits to me
- The complete list of items is indexable
- No CLS issue because the full list is rendered directly
I have that list of items filtered against values stored in the query parameters to manage the state in order to have shareable URLs
The issue now becomes that the component calling
useSearchParams to read the state needs to be wrapped in Suspense. And guestimating the height of my component for the fallback skeleton isn't an option. Instead, I expected to have the filtering done completely client-side, filtering the initial list that came from the serverThis feels like fixed probleme already, are there recipes with that kind of workflow? I must be missing something obvious
18 Replies
Pacific herring
Well, you can make the whole page dynamic by putting
await connection()
in the page.tsx file.
Doing that will remove the need for wrapping the component in a Suspense since the route will be dynamic.
await connection()
in the page.tsx file.
Doing that will remove the need for wrapping the component in a Suspense since the route will be dynamic.
Blue orchard beeOP
But I don't want it dynamic 🤔 Or I'm not understanding "dynamic" correctly
The full list of items is fine for the first render because it'll be 99% of the usage for people
The filtering would happen client side only, after the full list was rendered
CLS isn't really an issue because things would move below the fold, at least that was the case last time I had a look at what CLS cares about
The full list of items is fine for the first render because it'll be 99% of the usage for people
The filtering would happen client side only, after the full list was rendered
CLS isn't really an issue because things would move below the fold, at least that was the case last time I had a look at what CLS cares about
Pacific herring
you can do allat even if it's dynamic.
but just so we're clear, your issue is that you don't want to wrap the client component in a suspense boundary right?
but just so we're clear, your issue is that you don't want to wrap the client component in a suspense boundary right?
Blue orchard beeOP
Affirmative
Pacific herring
then I suggest making the route dynamic, cause tbh, there's no other way I'm aware off.
It even says so in the docs
It even says so in the docs
Pacific sand lance
i;m almost sure that you don;t need any meaningful skeleton to use search params in component, i used null as fallback few times and the only
issue i encountered was that component was pre-rendered within <template>if that doesnt work well for you, try using ppr if your nextjs version supports it
Blue orchard beeOP
I'm trying to understand what making it dynamic will entail
My absolute prerequisite is that the fetch still happens server side, making it dynamic would make a page render (still server side) for each combination of the queryparams that the users can come up with? Or am I missing something
Pacific sand lance
depends how you implement it
by default, upadting query params client-side won't cause
router refreshBlue orchard beeOP
router.replace('?foo') right?@Blue orchard bee My absolute prerequisite is that the fetch still happens server side, making it dynamic would make a page render (still server side) for each combination of the queryparams that the users can come up with? Or am I missing something
Pacific herring
Kinda, but it really depends on how you're filtering.
Calling router.push or router.replace will cause a re-render of the server component since Next.js treats both as route changes.
This happens with or without the await connection().
Since you want to filter purely on the client without triggering a server component re-render, you basically have two options:
1. Cache the function that fetches the data by adding the "use cache" directive. That way, even if the component re-renders, the fetch function won’t run again because its output is cached.
2. Keep everything on the client side.
Just store a value inside the client component, like:
const [filterBy, setFilterBy] = useState('')
Then do your filtering in the client.
The downside is that this filtering won’t be tied to the URL. So if the user manually types ?filterBy=whatever in the address bar, nothing will show up because it isn’t connected.
You can fix that by reading the initial filterBy from the searchParams prop in the page component on the first render. You can use it as the default value for the filterBy state.
Also, whenever the user filters, since you don’t want to re-render the server component, you can do a soft update by using window.history.pushState. That will update the filter query in the URL without re-rendering the page.
Calling router.push or router.replace will cause a re-render of the server component since Next.js treats both as route changes.
This happens with or without the await connection().
Since you want to filter purely on the client without triggering a server component re-render, you basically have two options:
1. Cache the function that fetches the data by adding the "use cache" directive. That way, even if the component re-renders, the fetch function won’t run again because its output is cached.
2. Keep everything on the client side.
Just store a value inside the client component, like:
const [filterBy, setFilterBy] = useState('')
Then do your filtering in the client.
The downside is that this filtering won’t be tied to the URL. So if the user manually types ?filterBy=whatever in the address bar, nothing will show up because it isn’t connected.
You can fix that by reading the initial filterBy from the searchParams prop in the page component on the first render. You can use it as the default value for the filterBy state.
Also, whenever the user filters, since you don’t want to re-render the server component, you can do a soft update by using window.history.pushState. That will update the filter query in the URL without re-rendering the page.
This will also remove the need for the useSearchParams hook
Blue orchard beeOP
Right, the intent was (still is) to save state for shareable URLs
I did have a working hand-made state management (w/
For science I replaced that with Nuqs which I'm guessing pushes using the browser history API directly since the fetch in server were not re-running anymore but that left me with my initial issue which was to go around having to use
I'll look into
I wonder whether Nuqs already supports reading the initial state without using
I did have a working hand-made state management (w/
router.replace) but thought I was doing something wrong because the fetches in server were running on every update. Glad to know that this is the intended behaviorFor science I replaced that with Nuqs which I'm guessing pushes using the browser history API directly since the fetch in server were not re-running anymore but that left me with my initial issue which was to go around having to use
Suspense (because the flickering on my filter UI/Skeleton isn't too pretty)I'll look into
'use state' next for educational purposes but it looks like the second suggestions is closer to what I needI wonder whether Nuqs already supports reading the initial state without using
useSearchParams 🤔Brown bear
You could use a loader to one-time parse location.search if you don’t want a reactive version:
https://nuqs.dev/docs/server-side#loaders
(it says server side, but you can run loaders from anywhere, and import them from 'nuqs' rather than 'nuqs/server')
https://nuqs.dev/docs/server-side#loaders
(it says server side, but you can run loaders from anywhere, and import them from 'nuqs' rather than 'nuqs/server')
But then you might get hydration issues