Reputation: 21615
I'm trying to wrap my head around server side rendering with Next.js 13.4 and the new app/
directory. To my understanding, every component is a server component (i.e. rendered server-side) by default.
However, we can use the 'use client'
directive to forcibly turn a component into a client component.
Below, I set up a simple "Hello World" component, first as a server component and then as a client component. In each case, I compare the page source code.
src/app/page.js
(server component)export default function Home() {
return (
<main>
<h1>Hello World</h1>
</main>
)
}
Chrome > view page source
<!DOCTYPE html>
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/_next/static/chunks/polyfills.js" noModule=""></script>
</head>
<body>
<main>
<h1>Hello World</h1>
</main>
<script src="/_next/static/chunks/webpack.js" async=""></script>
<script src="/_next/static/chunks/main-app.js" async=""></script>
<script>(self.__next_f = self.__next_f || []).push([0])</script>
<script>self.__next_f.push([1, "0:\"$L1\"\n"])</script>
<script>self.__next_f.push([1, "2:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/app-router.js\",\"chunks\":[\"webpack:static/chunks/webpack.js\"],\"name\":\"\",\"async\":false}\n4:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/error-boundary.js\",\"chunks\":[\"webpack:static/chunks/webpack.js\"],\"name\":\"\",\"async\":false}\n6:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/layout-router.js\",\"chunks\":[\"app-client-internals:static/chunks/app-client-internals.js\"],\"name\":\"\",\"async\":false}\n7:I{\"id\":\"(app-client)/"])</script>
<script>self.__next_f.push([1, "./node_modules/next/dist/client/components/render-from-template-context.js\",\"chunks\":[\"app-client-internals:static/chunks/app-client-internals.js\"],\"name\":\"\",\"async\":false}\n"])</script>
<script>self.__next_f.push([1, "1:[[],[\"$\",\"$L2\",null,{\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialHead\":[\"$L3\",null],\"globalErrorComponent\":\"$4\",\"notFound\":[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$L5\",\"$undefined\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}],\"asNotFound\":false,\"children\":[[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"hasLoading\":false,\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"childProp\":{\"current\":[[\"$\",\"main\",null,{\"children\":[\"$\",\"h1\",null,{\"children\":\"Hello World\"}]}],null],\"segment\":\"__PAGE__\"},\"styles\":[]}]}]}],null]}]]\n"])</script>
<script>self.__next_f.push([1, "5:[[[\"$\",\"meta\",null,{\"charSet\":\"utf-8\"}],null,null,null,null,null,null,null,null,null,null,[\"$\",\"meta\",null,{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],null,null,null,null,null,null,null,null,null,null,[]],[null,null,null,null],null,null,[null,null,null,null,null],null,null,null,null,null]\n3:[[[\"$\",\"meta\",null,{\"charSet\":\"utf-8\"}],[\"$\",\"title\",null,{\"children\":\"Create Next App\"}],[\"$\",\"meta\",null,{\"name\":\"description\",\"content\":\"Generated by create next app\"}],null,null,null,null,n"])</script>
<script>self.__next_f.push([1, "ull,null,null,null,[\"$\",\"meta\",null,{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],null,null,null,null,null,null,null,null,null,null,[]],[null,null,null,null],null,null,[null,null,null,null,null],null,null,null,null,null]\n"])</script>
</body>
</html>
src/app/page.js
(client component)'use client';
export default function Home() {
return (
<main>
<h1>Hello World</h1>
</main>
)
}
Chrome > view page source
<!DOCTYPE html>
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/_next/static/chunks/polyfills.js" noModule=""></script>
</head>
<body>
<main>
<h1>Hello World</h1>
</main>
<script src="/_next/static/chunks/webpack.js" async=""></script>
<script src="/_next/static/chunks/main-app.js" async=""></script>
<script>(self.__next_f = self.__next_f || []).push([0])</script>
<script>self.__next_f.push([1, "0:\"$L1\"\n"])</script>
<script>self.__next_f.push([1, "2:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/app-router.js\",\"chunks\":[\"webpack:static/chunks/webpack.js\"],\"name\":\"\",\"async\":false}\n4:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/error-boundary.js\",\"chunks\":[\"webpack:static/chunks/webpack.js\"],\"name\":\"\",\"async\":false}\n6:I{\"id\":\"(app-client)/./node_modules/next/dist/client/components/layout-router.js\",\"chunks\":[\"app-client-internals:static/chunks/app-client-internals.js\"],\"name\":\"\",\"async\":false}\n7:I{\"id\":\"(app-client)/"])</script>
<script>self.__next_f.push([1, "./node_modules/next/dist/client/components/render-from-template-context.js\",\"chunks\":[\"app-client-internals:static/chunks/app-client-internals.js\"],\"name\":\"\",\"async\":false}\n8:I{\"id\":\"(app-client)/./src/app/page.js\",\"chunks\":[\"app/page:static/chunks/app/page.js\"],\"name\":\"\",\"async\":false}\n"])</script>
<script>self.__next_f.push([1, "1:[[],[\"$\",\"$L2\",null,{\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialHead\":[\"$L3\",null],\"globalErrorComponent\":\"$4\",\"notFound\":[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$L5\",\"$undefined\",[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]]}]}],\"asNotFound\":false,\"children\":[[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"hasLoading\":false,\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"childProp\":{\"current\":[[\"$\",\"$L8\",null,{\"params\":{},\"searchParams\":{}}],null],\"segment\":\"__PAGE__\"},\"styles\":[]}]}]}],null]}]]\n"])</script>
<script>self.__next_f.push([1, "5:[[[\"$\",\"meta\",null,{\"charSet\":\"utf-8\"}],null,null,null,null,null,null,null,null,null,null,[\"$\",\"meta\",null,{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],null,null,null,null,null,null,null,null,null,null,[]],[null,null,null,null],null,null,[null,null,null,null,null],null,null,null,null,null]\n3:[[[\"$\",\"meta\",null,{\"charSet\":\"utf-8\"}],[\"$\",\"title\",null,{\"children\":\"Create Next App\"}],[\"$\",\"meta\",null,{\"name\":\"description\",\"content\":\"Generated by create next app\"}],null,null,null,null,n"])</script>
<script>self.__next_f.push([1, "ull,null,null,null,[\"$\",\"meta\",null,{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],null,null,null,null,null,null,null,null,null,null,[]],[null,null,null,null],null,null,[null,null,null,null,null],null,null,null,null,null]\n"])</script>
</body>
</html>
I'm confused because it appears as though the client component has its HTML pre-rendered, but "Hello World" clearly exists in the source code. I expected to see something akin to React's root DOM node - basically an empty div waiting on some HTML to be injected. What am I missing here?
Upvotes: 5
Views: 12343
Reputation: 11
Server and Client Components are rendered differently during Static Rendering:
Now, with Server and Client Components, React can render on the client and the server meaning you can choose the rendering environment at the component level.
By default, the app
router uses Server Components, allowing you to easily render components on the server and reducing the amount of JavaScript sent to the client.
Upvotes: 1
Reputation: 1702
Client components allow the use of browser APIs and are mainly used to handle user interactions. However, NextJs has server-side rendering (SSR).
Therefore:
Server components are used to handle logic that does not require client involvement (data fetching, non-interactive UI, etc.). Since there is no client-side involvement, there is no need to send these JavaScript packages to the client.
⚠️But be careful: if you import a server component directly in a client component, it will become a client component as well.
But anyway, for normal use of server components:
So in NextJs both client and server components are pre-rendered on the server, which improves SEO and is one of the original purposes of NextJs.
If you want to see "blank" in the source code, you need to use the "client-only" component:
useState
to return empty for the first render, and useEffect
to change the state to trigger a rerender at the right time.'next/dynamic'
.But of course you don't want to do that in most cases. ;-)
Upvotes: 0
Reputation: 49182
Client Components
Client Components enable you to add client-side interactivity to your application. In Next.js, they are pre-rendered on the server and hydrated on the client. You can think of Client Components as how components in the Pages Router have always worked.
Upvotes: 0