Next.js 13 introduced React Server Components, giving developers the power to choose where and how to render components—either on the server for performance or on the client for interactivity. This flexibility allows us to build apps that combine speed and dynamic capabilities.
In this article, we’ll explore not just the basics, but also dive into how to use server components within client components—a common need when building dynamic, efficient apps.
Understanding Server Components
Server components are rendered entirely on the server and don’t require any client-side JavaScript. They’re perfect for static content like headers, footers, or even data-driven components that don't need user interaction.
Example: Simple Server Component
// app/components/Header.js
export default function Header() {
return (
<header>
<h1>My Static Header</h1>
</header>
);
}
This component is rendered on the server and doesn't involve any client-side interaction, meaning it loads faster with less JavaScript.
Benefits of Server Components
- Reduced JavaScript Payload: Server components reduce the amount of JavaScript sent to the browser.
- Improved Data Fetching: Server components can fetch data closer to the database, reducing network latency.
Fetching Data in a Server Component
// app/components/PostList.js
export default async function PostList() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return (
<ul>
{posts.slice(0, 5).map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
This PostList component fetches data on the server and sends the pre-rendered HTML to the client, ensuring faster load times.
When to Use Client Components
Client components are essential when you need interactivity, such as form inputs, event listeners, or dynamic content. These components use JavaScript on the client to handle user interactions.
Example: Client Component for Interactivity
// app/components/SearchBar.js
'use client'; // This makes the component a client component
import { useState } from 'react';
export default function SearchBar() {
const [searchTerm, setSearchTerm] = useState('');
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<p>Searching for: {searchTerm}</p>
</div>
);
}
The SearchBar is interactive, so it needs to be a client component. You can use the useState hook and other React hooks only in client components.
You might have a use-case to combine Server and Client Components, so let's talk on how to do that next:
Combining Server and Client Components
A core strength of Next.js 13 is the ability to combine server and client components. A best practice is to use server components by default and push client components as deep as possible into your component tree.
Example: Combining Server and Client Components
// app/layout.js
import SearchBar from './components/SearchBar';
export default function Layout({ children }) {
return (
<div>
<header>My Blog</header>
<SearchBar /> {/* Client component for interactivity */}
{children}
</div>
);
}
The SearchBar component handles client-side interactivity, while the rest of the layout is server-rendered, offering a balance between performance and interactivity.
On the other-way round, you might have a use-case to use server component inside a client component. Let's check out how to do that.
How to Use Server Components Inside Client Components
It’s important to understand that server components can be nested inside client components, but not imported directly into them. To include a server component in a client component, you pass it as children or a prop to avoid breaking the boundary between the two.
Example: Passing a Server Component to a Client Component
Here’s a real-world example where a server component is passed as a child to a client component:
// app/components/Profile.js (Server Component)
export default function Profile({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
// app/components/Dashboard.js (Client Component)
'use client';
import { useState } from 'react';
export default function Dashboard({ children }) {
const [showProfile, setShowProfile] = useState(false);
return (
<div>
<button onClick={() => setShowProfile(!showProfile)}>
{showProfile ? 'Hide Profile' : 'Show Profile'}
</button>
{showProfile && <div>{children}</div>} {/* Server component passed as children */}
</div>
);
}
// app/page.js (Main Page using both components)
import Profile from './components/Profile';
import Dashboard from './components/Dashboard';
export default async function Page() {
const user = { name: 'John Doe', email: 'john@example.com' }; // Static example
return (
<div>
<Dashboard>
<Profile user={user} /> {/* Passing the server component to client */}
</Dashboard>
</div>
);
}
In the above example:
- Profile is a server component, fetching data or displaying static content.
- Dashboard is a client component handling interactions (showing/hiding the profile).
- The Profile server component is passed as children to the Dashboard client component.
This pattern allows you to use the benefits of server rendering (less JavaScript, improved performance) while still having client-side interactivity.
Third-Party Libraries and Client Components
Many third-party libraries like authentication providers or UI components rely on React hooks, which can only be used in client components. Here’s how you can work around that limitation by wrapping third-party libraries inside client components:
Example: Using a Third-Party Carousel
// app/components/Carousel.js
'use client';
import Carousel from 'react-slick';
export default function MyCarousel() {
const settings = { dots: true, infinite: true };
return (
<Carousel {...settings}>
<div><h3>Slide 1</h3></div>
<div><h3>Slide 2</h3></div>
</Carousel>
);
}
// app/page.js
import MyCarousel from './components/Carousel';
export default function Page() {
return (
<div>
<h1>Welcome to the App</h1>
<MyCarousel />
</div>
);
}
By wrapping the third-party react-slick carousel in a client component, we can use it in the server-rendered page while still accessing client-side features like interactivity.
Handling Props Between Server and Client Components
When passing data between server and client components, the props must be serializable (e.g., strings, numbers, booleans). Complex objects like functions or instances of classes can’t be passed.
Example: Passing Data from Server to Client
// app/page.js (Server Component)
import UserCard from './components/UserCard';
export default async function Page() {
const user = { name: 'Jane Doe', age: 30 }; // Simple serializable data
return (
<div>
<h1>Welcome to the App</h1>
<UserCard user={user} /> {/* Passing serializable data to client component */}
</div>
);
}
// app/components/UserCard.js (Client Component)
'use client';
export default function UserCard({ user }) {
return (
<div>
<h2>{user.name}</h2>
<p>Age: {user.age}</p>
</div>
);
}
The UserCard client component can now dynamically render the data passed from the server component while ensuring that everything remains serializable and thus passes through the server-client boundary without issues.
With all said, it would be interesting to conclude this with best practises. Let's move to that next:
Best Practices for Server and Client Component Composition
Here are a few tips for composing server and client components effectively:
Default to Server Components: Use server components wherever possible for static or data-driven content to reduce JavaScript load and improve performance.
Use Client Components for Interactivity: Only use client components where user interaction or browser-specific APIs are needed.
Move Client Components Down the Tree: Push client components as deep into the component tree as possible. This allows more of your app to be rendered on the server, boosting performance.
Pass Server Components as Children: If a server component needs to be used within a client component, pass it as children or a prop instead of directly importing it.
Final word: Striking the Balance Between Performance and Interactivity
With Next.js 13, you have the flexibility to render components on both the server and the client. By defaulting to server components for static content and client components for interactivity, and by managing the boundary between the two carefully, you can build apps that are both fast and dynamic.
By following the patterns and examples here—like passing server components into client components and combining them thoughtfully—you’ll be able to leverage the full power of Next.js 13 to create highly performant, interactive web applications.
Happy coding
I am Michael.
Top comments (0)