06 Dec 2024 - 4 min read
In my previous article, Server-Side Rendering: Not the PHP We Used to Know, I explored how SSR today differs from the PHP we used to use, highlighting how Next.js SSR with server and client components is a game changer. Now, let's dive into a practical demonstration of these concepts and explain how hydration, Suspense, and other key terminologies work within this architecture.
To effectively combine server-side efficiency with client-side interactivity in a Next.js application, it's essential to understand the different types of rendering that Next.js offers. Each type plays a unique role in optimizing performance, SEO, and user experience.
In traditional client-side React applications, all components run in the browser, limiting the ability to handle backend logic directly. Next.js changes this by allowing developers to define server components, which execute on the server, and client components, which handle interactivity and state management on the client. This clear separation optimizes performance and enhances security.
// Fetching data on the server const fetchPosts = async () => { return await prisma.post.findMany({ orderBy: { createdAt: 'desc' } }); }; const PostsPage = async () => { const posts = await fetchPosts(); return ( <Suspense fallback={<Spinner />}> <PostsList posts={posts} /> </Suspense> ); };
'use client'; const PostsList = ({ posts }) => ( <div> {posts.map((post) => ( <h2 className="text-xl font-bold text-purple-900" key={post.id}> {post.title} </h2> ))} </div> );
The use client directive serves as a bridge between server and client components, enabling the seamless flow of data from the server to the client while maintaining React's unidirectional data flow. This ensures that server-rendered data can be passed down to client components for further interactivity or rendering, aligning with React's declarative programming model.
Hydration is the process where the client-side React app takes over the server-rendered HTML, attaching event listeners and making the app interactive. Here's what happens during hydration:
Event handlers can be defined on server components and passed to client components, leveraging the 'use server' directive for security and performance.
import CreateForm from '../components/CreateForm'; import { PostSubmitData } from '@/types/Post'; import { redirect } from 'next/navigation'; import prisma from '@/lib/prisma'; const CreatePage = () => { const submitForm = async (data: PostSubmitData) => { 'use server'; // Ensures this runs only on the server const keywordsArray = data.keywords.split(' ').map((keyword) => keyword.trim()); await prisma.post.create({ data: { title: data.title, content: data.content, keywords: keywordsArray }, }); redirect('/posts'); }; return ( <div className="container mx-auto p-4"> <h1 className="text-2xl font-bold mb-4">Create a Post</h1> <CreateForm submitForm={submitForm} /> </div> ); }; export default CreatePage;
React's Suspense API allows developers to defer rendering parts of the UI until the required data or code is ready.
const PostsPage = async () => { const posts = await fetchPosts(); return ( <Suspense fallback={<Spinner />}> <PostsList posts={posts} /> </Suspense> ); };
In this example:
Next.js enables developers to efficiently combine server-side and client-side components, optimizing both performance and interactivity. By leveraging SSR, SSG, and CSR, Next.js offers flexible rendering options that suit different application needs. The integration of server and client components facilitates smooth data flow, allowing developers to create dynamic, high-performance applications with ease. This architecture marks a significant evolution in modern web development, empowering developers to build scalable, secure, and engaging user experiences.