Why is this component not fully SSRed, how can I do it and what learnings can I take from it?

So, My understanding is: export default function ServerComponent(){ return <div> // some normal HTML tags <ClientComponent/> </div>
} Then this file will have things SSRed till it hits that Client Component boundary, am I right? So the things before it should be in HTML response but they aren't!
10 Replies
Rivenris
Rivenris3w ago
It's really hard to tell what is not going well without more details. General rule is - yes, server components go through immediately. You should see html and body tags in the response at least from the layout. Depending on your config and actual code, the dynamic content will be streamed afterwards or waited upon before nested components will show up. I'd investigate the code for bottlenecks and async calls. Also check where is the response html cutoff, because that component will probably be the issue.
Rajneesh Mishra
Rajneesh MishraOP3w ago
Okay, now I can send code of the concerned files. I wanted to know if I was completely wrong
import Dashboard from "@/components/Dashboard";
import axiosInstance from "@/lib/axiosInstance";
export const dynamic = "force-dynamic";

export default async function DashboardPage() {
try {
const response = await axiosInstance.get(
`${process.env.NEXT_PUBLIC_API_URL}/api/users/user-details-dashboard`
);
if (response.status === 200) {
return <Dashboard userDetails={response.data} />;
}
} catch (error) {
throw new Error(JSON.stringify(error));
}
}
import Dashboard from "@/components/Dashboard";
import axiosInstance from "@/lib/axiosInstance";
export const dynamic = "force-dynamic";

export default async function DashboardPage() {
try {
const response = await axiosInstance.get(
`${process.env.NEXT_PUBLIC_API_URL}/api/users/user-details-dashboard`
);
if (response.status === 200) {
return <Dashboard userDetails={response.data} />;
}
} catch (error) {
throw new Error(JSON.stringify(error));
}
}
The above is page.tsx inside the dashboard which is just inside the app folder for the "/dashboard" route.
import DashboardLoginButton from "./DashboardLoginButton";
import { User } from "@/types/user";

export default function Dashboard({ userDetails }: { userDetails: User }) {
return (
<div className="h-[90vh] p-5 bg-purple-50 flex flex-col">
<div className="bg-white shadow-sm rounded-md flex-grow overflow-hidden flex flex-col">
<div className="overflow-x-auto flex-grow">
<table className="w-full text-left">
<thead className="bg-gray-200">
<tr>
<th className="py-3 px-4 text-xs">GSTIN</th>
<th className="py-3 px-4 text-xs">State</th>
<th className="py-3 px-4 text-xs">Company</th>
<th className="py-3 px-4 text-xs">Action</th>
</tr>
</thead>
<tbody>
{userDetails?.gstins?.map((gstin, index) => (
<tr key={index} className="border-b hover:bg-gray-100">
<td className="py-3 px-4">
<div className="font-semibold text-xs">{gstin.gstin}</div>
</td>
<td className="py-3 px-4 text-xs">{gstin.state}</td>
<td className="py-3 px-4 text-xs">{gstin.company_name}</td>
<td className="py-3 px-4">
<DashboardLoginButton gstin={gstin} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
import DashboardLoginButton from "./DashboardLoginButton";
import { User } from "@/types/user";

export default function Dashboard({ userDetails }: { userDetails: User }) {
return (
<div className="h-[90vh] p-5 bg-purple-50 flex flex-col">
<div className="bg-white shadow-sm rounded-md flex-grow overflow-hidden flex flex-col">
<div className="overflow-x-auto flex-grow">
<table className="w-full text-left">
<thead className="bg-gray-200">
<tr>
<th className="py-3 px-4 text-xs">GSTIN</th>
<th className="py-3 px-4 text-xs">State</th>
<th className="py-3 px-4 text-xs">Company</th>
<th className="py-3 px-4 text-xs">Action</th>
</tr>
</thead>
<tbody>
{userDetails?.gstins?.map((gstin, index) => (
<tr key={index} className="border-b hover:bg-gray-100">
<td className="py-3 px-4">
<div className="font-semibold text-xs">{gstin.gstin}</div>
</td>
<td className="py-3 px-4 text-xs">{gstin.state}</td>
<td className="py-3 px-4 text-xs">{gstin.company_name}</td>
<td className="py-3 px-4">
<DashboardLoginButton gstin={gstin} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
The above is the Dashboard.tsx component which is getting used in the "page.tsx" file inside the "/dashboard" folder
Rajneesh Mishra
Rajneesh MishraOP3w ago
And the below is DashboardLoginButton component's code
Rajneesh Mishra
Rajneesh MishraOP3w ago
Directory structure:- |-> src |-> app |-> dashboard |-> layout.tsx(Server Component, don't worry I am not that big of a noob) |-> page.tsx(This contains the Dashboard Page component which a RSC and makes the backend call) |-> components (at the same level as app) |-> Dashboard.tsx (This contains the table with hardcoded headers) |-> DashboardLoginButton.tsx(The client component getting rendered inside the Dashboard.tsx server component) This is the response in the "dashboard" document.
Rajneesh Mishra
Rajneesh MishraOP3w ago
No description
Rivenris
Rivenris3w ago
@Rajneesh Mishra So, Page is SSR-ed, just not all at the same time. First, the known html blocks are sent (the original output) and after that you see a bunch of scripts at the end of html - this is next SSR streaming the content, that were added to html once axios call gets user data. In other words, first you get html that doesn't need to be delayed, and after that, using the existing request, next adds components rendered a bit later. This is expected behavior and how next deals with waiting for data response. Things to note here: 1. Axios call still happens on the server, and only added html is streamed to browser. 2. You can (and often should) introduce some form of loading indicator - e.g. using Suspense to render fallback 'skeleton' UI. (more on that here: https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#streaming-with-suspense) 3. If the api url points to the same next app as the UI, I would strongly suggest replacing axios call with direct code execution on server component - I won't get into detail here unlesss you want to talk about it further.
Routing: Loading UI and Streaming | Next.js
Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.
Rajneesh Mishra
Rajneesh MishraOP3w ago
Hello, thank you for the reply and sorry for the late reply of mine, I was busy yesterday. Yes I do want to talk further. What I expected was that all the data will be in HTML once the axios call fetches it. Then the whole HTML file with the hardcoded initial data will be sent. I am showing a loading UI, I have a loading.tsx file in the root of my app directory so it works. I was expecting the table tags to be in HTML and then the table body to be streamed in, is there a way to do it? I am gonna look into the docs link you shared after it. Thank you. This is the file in which I want the 'tbody' tag to slowly stream while I show Loading Data, so should I add a Suspense Boundary there? Also, I have a python backend from where the data is being fetched. Before(like 15-20 days ago) when I used to make a request the data was literally engraved in the HTML itself, I remember the joy of seeing it inside the HTML but now all of a sudden this happened. In case its relevant I'll share you the root layout of my app and the providers file surrounding it.
import localFont from "next/font/local";
import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
import Providers from "@/components/Providers";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});

export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Toaster />
<Providers>{children}</Providers>
</body>
</html>
);
}
import localFont from "next/font/local";
import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
import Providers from "@/components/Providers";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});

export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Toaster />
<Providers>{children}</Providers>
</body>
</html>
);
}
This is the root layout
"use client";

import { persistor, store } from "@/store";
import React from "react";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { SessionProvider } from "@/context/SessionContext";
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<SessionProvider>
{persistor ? (
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
) : (
<>{children}</>
)}
</SessionProvider>
</Provider>
);
}
"use client";

import { persistor, store } from "@/store";
import React from "react";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { SessionProvider } from "@/context/SessionContext";
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<Provider store={store}>
<SessionProvider>
{persistor ? (
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
) : (
<>{children}</>
)}
</SessionProvider>
</Provider>
);
}
This is the Providers.tsx file in the components file, inside src but outside app
Rivenris
Rivenris3w ago
I gave it some thought and I thought this may give you perpective - here's how I would write this UI. Option 1: push data loading to rows component
// dashboard.tsx
async function DashboardPage(params) {
const pageNo = await (params).page;
// no data loading here, but still server comp
return <Table>
<Suspense fallback={<TableDataMock />}>
<TableDataRows page={page} />
</Suspense>
</Table>
}


// TableData.tsx
async function TableDataRows({page}) {
// We get data here
const data = await getMeData(page)
return data.map(item => <tr key={item.id}>
<td>{item.name}</td>
...
</tr>)
}
// dashboard.tsx
async function DashboardPage(params) {
const pageNo = await (params).page;
// no data loading here, but still server comp
return <Table>
<Suspense fallback={<TableDataMock />}>
<TableDataRows page={page} />
</Suspense>
</Table>
}


// TableData.tsx
async function TableDataRows({page}) {
// We get data here
const data = await getMeData(page)
return data.map(item => <tr key={item.id}>
<td>{item.name}</td>
...
</tr>)
}
In here data loading is done only in context of actual rows. Table exists outside of loaded scope, so it will show headers etc way before data is rendered. Everything is server-sided, but you can add e.g. ActionMenu component that is client-side and does some client-side things based on loaded data. Option 2: Load data table level, use the same table headers for both fallback and loaded state
// dashboard.tsx
async function DashboardPage(params) {
const pageNo = await (params).page;
// no data loading here, but still server comp
return <Suspense fallback={<TableMock />}>
<Table page={page} />
</Suspense>
}

async function Table({page}) {
const data = await getMeData(page);
return <table>
<TableHead />
{data.map(item => <tr>...</tr>)}
</table>
}

// tablehead.tsx
function TableHead() {
return <thead>
...
</thead>
}

// tablemock.tsx
function TableMock() {
return <table>
<TableHead />
<tr><td>Loading...</td></tr>
</table>
}
// dashboard.tsx
async function DashboardPage(params) {
const pageNo = await (params).page;
// no data loading here, but still server comp
return <Suspense fallback={<TableMock />}>
<Table page={page} />
</Suspense>
}

async function Table({page}) {
const data = await getMeData(page);
return <table>
<TableHead />
{data.map(item => <tr>...</tr>)}
</table>
}

// tablehead.tsx
function TableHead() {
return <thead>
...
</thead>
}

// tablemock.tsx
function TableMock() {
return <table>
<TableHead />
<tr><td>Loading...</td></tr>
</table>
}
Now, the cool thing is, when you navigate from dashboard/1 to dashboard/2, next will skip rendering of the page, table head etc. and send only diff that needs to be replaced in the target - namely table rows. So you don't have to worry about deciding which html is sent when - it's done on next-side automatically. Note, that I wrote everything to be server components. This is how I personally work with Next - push as much as possible to server-side. This speeds up data-loading and rendering by a lot. Additionally I would suggest: - Not worrying about getting response as full html from next each time. Unless you are focusing on SEO, getting the table content indexed by bots etc. you don't need that - Use Server Actions to modify data. Then revalidate the data on server action. This will cause next to calculate the diff of html and send update to the SSR-ed UI automatically. Again, no need to worry about pushing updated data through client-side states only to show newest version after save - Push state of auth as low as you can - your page may not need to know who is logged in. Button in the navbar needs to know this, but not necessarily the page itself. It may be a bit counterintuitive as in the classic single page apps built on React you need to control where data is loaded and deduplicate those requests. On SSR-ed Next.js you don't need that if you handle your fetch cache correctly. You can push data loading as low as you wish. Next.js UI revalidation model is phenomenal and I'd use it over client-side data state in almost all cases, because it allows you to skip update logic on client in most of the cases. That is why I choose to use server-side rendering for as much as I can.
Rajneesh Mishra
Rajneesh MishraOP2w ago
Okay I will try to mock it separately and then after that will put it into my codebase. Could you decipher why Next.js is behaving the way it is? Because oddly enough, I used to get Table Headers in the HTML itself before but after a few changes it stopped happening and even the table headers became client side rendered. Also, importing client components in a server component doesn't make it a client component right? Sure, I'll try it Although, in the table there are multiple Login buttons in each row, actually I'll send you the original UI in the DMs And thank you very much for putting your time into it
Rivenris
Rivenris2w ago
Also, importing client components in a server component doesn't make it a client component right?
No, client component used in server component simply makes cutoff point for next to know where to start sending react js code to client. Server components will only have react code on server. Client will live on both.
Could you decipher why Next.js is behaving the way it is? Because oddly enough, I used to get Table Headers in the HTML itself before but after a few changes it stopped happening and even the table headers became client side rendered.
The thing to remember is that async call will delay the html and push it into stream afterwards. Previously you had axios call right at the start of the page, which makes pretty much everything afterwards (including jsx/html) delayed. Pushing async call lower will make the top stuff generate asap. In general, await in Next 15 is the way for Next to think "Ok, let's send what we have for now, and from now on stream the lower part of the tree once promise is resolved"
Although, in the table there are multiple Login buttons in each row, actually I'll send you the original UI in the DMs
Shouldn't be a problem here. I used Clerk which provides it's own login button in similar way, where the login button was placed all over the page. You can still handle it with one server action, used accross multiple components.

Did you find this page helpful?