Next.js Discord

Discord Forum

Advice on tabbed interfaces and data loading

Unanswered
Pacific saury posted this in #help-forum
Open in Discord
Pacific sauryOP
Hey folks, I wanted to get some advice on the best way(s) to implement a specific UX in App Router.

My main page should have a set of tabs at the top that filter the results in a table below. The rows in the table are stored in a single DB table, and the tabs are stored in a different DB table (they are linked through foreign keys).

My first instinct is to use a query param to keep track of which tab is selected. When implementing this though, I ran into some issues with needing to use client code in a server component (specifically window.history and useSearchParams). Seeing as this wasn't really working, I tried moving to using the searchParams prop for the page and calling redirect() to change the query param. The problem there is that (I think) I would have to re-fetch the tabs and user data from the DB each time I re-directed, which I would prefer not to do.

It is worth noting that I considered factoring the tab interface into its own client component, but sharing state with the table was a mess and resulted in something like this:
<page.tsx> // Server component
  <Navigation.tsx> // Client component
    <Table.tsx />
  </Navigation>
</page>


Is there some other way of approaching this that I haven't considered?

11 Replies

I implemented something similar using dynamic routes/dynamic params.

I prefer this method over searchParams since it I know what the values are I can generate them statically at build time, and use ISR to revalidate the data if it ever changes.
And even if I don't know the value (like if a new one is added in the DB, and the data is revalidated) as long as I'm not declaring dynamicParams = false the page will still be generated on-demand with that value
Pacific sauryOP
Thanks for the input, @Plague!

Yeah, I don't think I can use ISR here since the table's records are dependent on which user is signed-in (sorry that I forgot to mention this part).

It's worth mentioning that I'm not really using caching in my app right now (I have export const dynamic = "force-dynamic"; set in the root layout), mainly because the data is so user-dependent.
Pacific sauryOP
I'm on Next 14.2
As far re-fetching the data every time you redirect, if the data is truly dynamic and changes frequently then this is unavoidable, but if the data doesn't change frequently and only depends on the sign-in user, you can implement caching techniques like unstable_cache() and react's cache() function.

React's cache is bascially a memoize function for data fetches not using the fetch API and are called within react components

unstable_cache() is an aggresive persistent cache, that can be revalidated either time-based or on-demand
Pacific sauryOP
Got it. As I understand it cache() only works for multiple fetches of the same data in a single render. Is this correct? If so, not sure if it meets my needs, since I'm only fetching each piece of data once right now anyhow.

unstable_cache could be something to look into. Theoretically, the data could be changing frequently in a single user session, but it would mostly be only a few updates to that data per session (sometimes no updates at all). So as long as I bust the cache when running my server actions (which is the only place the data should be changing), then it should work.

Is it worth exploring a more client-side approach, where I get the majority of the data upfront and just switch between the data client-side? There isn't really a need for progressive enhancement or worrying about disabled JS for my use case.
@Pacific saury Got it. As I understand it `cache()` only works for multiple fetches of the same data in a single render. Is this correct? If so, not sure if it meets my needs, since I'm only fetching each piece of data once right now anyhow. `unstable_cache` could be something to look into. Theoretically, the data could be changing frequently in a single user session, but it would mostly be only a few updates to that data per session (sometimes no updates at all). So as long as I bust the cache when running my server actions (which is the only place the data should be changing), then it should work. Is it worth exploring a more client-side approach, where I get the majority of the data upfront and just switch between the data client-side? There isn't really a need for progressive enhancement or worrying about disabled JS for my use case.
As I understand it cache() only works for multiple fetches of the same data in a single render. Is this correct? If so, not sure if it meets my needs, since I'm only fetching each piece of data once right now anyhow.

Yes, that is correct.

unstable_cache could be something to look into. Theoretically, the data could be changing frequently in a single user session, but it would mostly be only a few updates to that data per session (sometimes no updates at all). So as long as I bust the cache when running my server actions (which is the only place the data should be changing), then it should work.

Yeah this approach works great.

Is it worth exploring a more client-side approach, where I get the majority of the data upfront and just switch between the data client-side? There isn't really a need for progressive enhancement or worrying about disabled JS for my use case.

Yeah there is nothing wrong with this approach either as long as you fetch the data on the server, but it wouldn't solve the issue of multiple requests for the data, since as it updates you will need to re-fetch that data.

Depending on how your app is setup and you use-case, it sounds like it would still decrease the overall amount of requests, so you have to be the judge of that, but the idea is sound.
Pacific sauryOP
it wouldn't solve the issue of multiple requests for the data, since as it updates you will need to re-fetch that data.

Yeah, I know I would still have to revalidate the path or data when it changes, but it shouldn't cause extra requests when the tab or query param changes when doing it client-side right?

Any other advice on how to structure the app if I go with the client-side approach? Is there any better way to get the correct tab identifier from the navigation to the table other than nesting the components?
@Pacific saury > it wouldn't solve the issue of multiple requests for the data, since as it updates you will need to re-fetch that data. Yeah, I know I would still have to revalidate the path or data when it changes, but it shouldn't cause extra requests when the tab or query param changes when doing it client-side right? Any other advice on how to structure the app if I go with the client-side approach? Is there any better way to get the correct tab identifier from the navigation to the table other than nesting the components?
Yeah, I know I would still have to revalidate the path or data when it changes, but it shouldn't cause extra requests when the tab or query param changes when doing it client-side right?

Not if the data is fetched in a dynamic server component or from the client. query param would re-render the dynamic server component, and client-side component are always dynamic by nature.

Any other advice on how to structure the app if I go with the client-side approach? Is there any better way to get the correct tab identifier from the navigation to the table other than nesting the components?

it's subjective to the use case and behavior you're looking for, but in most cases like the one you are describing searchParams or react state are neccessary, both of which will cause re-fetches of data on navigation.

Look into unstable_cache() with the cache busting like you mentioned, just make sure you don't cache too much data, because (at least on vercel) every 8KB of data read from the cache = 1 data cache read, and you get 1 million per month on hobby
Pacific sauryOP
Sounds good, thanks for the help!