Server and Client Components

React 18 introduced Server Components(RFC), which aims to combine the rich interactivity of client-side apps with the improved performance and benefits of server-side rendering.

Server-side rendering(SSR) is the process of generating the HTML on the server and sending it to the browser to be hydrated. During hydration, React will attempt to attach event listeners to the pre-built HTML to make it interactive. We can think of the traditional SSR process as Client Components.

On the other hand, React Server Components are always rendered on the server and then sent to the browser. Therefore, Server Components have full access to the backend infrastructure but not to the browser API.

Client and Server Components can live in harmony under a single DOM tree and complement each other to bring the best of each process to improve your application’s performance and user experience.


On this demo, we'll explore the principles of server and client components by fetching data from an API and using Client and Server components to make our app interactive.

To create a new Next.js project using the app directory, run:

npx create-next-app@latest --experimental-app

In Next.js 13 (beta) all components inside the app directory are React Server Components by default. Including special files and colocated components, this is a big change on how we've been thinking about component architecture.

This is the final file tree of our project. They are all server components, except for counter which is using hooks and is a client component.

Edit Server Components

Root directory

├── app │ ├── blog │ │ └── [slug] │ │ └── page.tsx │ ├── components │ │ ├── AppBar │ │ ├── Card │ │ ├── Counter │ │ ├── PopularFeed │ │ └── PostFeed │ ├── head │ ├── layout │ └── page.tsx ├── pages │ └── api └── next.config.js

Server Components

Server Components are always rendered on the server, they can interact directly with the backend to fulfill their needs. They can fetch data from a database or API, access the file system directly, or process expensive computations that require large dependencies. The output of Server Components is streamed to the client and cached.

Let's fetch data on the server using a Server component. For layouts and pages Next.js will automatically dedupe requests in a tree.

PostFeed.tsx

const fetchPosts = async (): Promise<IPost[]> => { const url = 'https://jsonplaceholder.typicode.com/posts/'; const data = await fetch(url); return data.json(); }; export default async function Page() { const posts = await fetchPosts(); return ( <main> {posts?.map(post => { return <Card key={post.id} post={post} />; })} </main> ); }

Card.tsx

import Link from 'next/link'; import { IPost } from '@/types/blog.types'; import Counter from '../Counter'; interface Props { post: IPost; dense?: boolean; } export default function Card({ post, dense }: Props) { if (dense) { return ( <div> <Link href={`/blog/${post.id}`}>{post.title}</Link> </div> ); } // Counter is a client component return ( <article> <h2>{post.title}</h2> <p>{post.body}</p> <Counter /> <Link href={`/blog/${post.id}`}>Go to article...</Link> </article> ); }

blog/[slug].tsx

import { IPost } from '@/types/blog.types'; const fetchPost = async (slug: string): Promise<IPost> => { const rootUrl = 'https://jsonplaceholder.typicode.com/posts/'; const data = await fetch(`${rootUrl}${slug}`); return data.json(); }; interface Props { params: { slug: string }; } export default async function PostPage({ params }: Props) { const post = await fetchPost(params.slug); return ( <div> <h1>{post.title}</h1> <p>{post.body}</p> </div> ); }

Streaming

Streaming allows you to break down the page’s HTML into smaller chunks and progressively send those chunks to the client. This enables the page to be rendered progressively by sections, instead of waiting for all the data to be fetched before rendering the HTML for a page like in the SSR world.

We can use React Suspense to display a fallback until its children have finished loading. This comes in handy when making an asynchronous action, like making a query to the db.

page.tsx

import { Suspense } from 'react'; import PostFeed from './components/PostFeed'; import PopularFeed from './components/PopularFeed'; export default async function Page() { return ( <main> <Suspense fallback={<p>Loading...</p>}> <PostFeed /> </Suspense> <Suspense fallback={<p>Loading...</p>}> <PopularFeed /> </Suspense> </main> ); }

With Server Components the initial page load is faster and large dependencies that previously would impact the JS bundle size on the client can remain entirely on the server.


Client Components

We can think of Client Components as the traditional SSR process, they are pre-rendered on the server and hydrated on the client. We should use them when we need to add interactivity into our UI.

Common use cases for using Client Components:

  • If we need state and lifecycle effects(useState, useReducer, useEffect)
  • If we use browser-only APIs
  • If we need interactivity with event listeners (onClick, onChange, etc)
  • If we use React Class components

We can make a Client Component by using the use client directive at the top of the file before any imports. These components can live anywhere, they don't need to live inside the app/ directory.

Counter.tsx

'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return ( <section> <p>You clicked {count} times.</p> <button onClick={handleClick}>Click me</button> </section> ); }

Avoid converting a Server Component into a Client Component because you need a client-side feature. Instead make that block of code a Client Component.

Move Client Components to the leaves of your component tree where possible to improve the performance.


Importing Client and Server components

Server and Client Components can be interleaved in the same component tree. React will merge both environments behind the scenes. Importing a Server Component in a Client Component will not work. Instead pass the Server component as a child prop of a Client Component.

ClientComponent

'use client'; export default function ClientComponent({ children }) { return <>{children}</>; }

page

import ClientComponent from './ClientComponent'; import ServerComponent from './ServerComponent'; // Pages are Server Components by default export default function Page() { return ( <ClientComponent> <ServerComponent /> </ClientComponent> ); }

On the other hand, using a client component inside a server component is very straightforward. In our project, please view how the Counter component is inside the Card component.


Resources

Checkout the intro video to React Server components. The RFC, It’s a very interesting thread, with a lot of valuable info. Demo to server components by the React team.

Link to github source code example.

Edit Server Components