Next.js SSR in Action: Demonstrating Hydration, Suspense, and Data Flow Architecture

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.




Combining Server-Side Efficiency with Client-Side Interactivity

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.


  • Static Site Generation (SSG): Pages are pre-rendered at build time, meaning the content is generated once during the build process and the same content is served to every request. This method is best for content that doesn’t change frequently and doesn’t require real-time data fetching.
  • Server-Side Rendering (SSR): Pages are rendered dynamically on the server each time a request is made. This ensures that content is always fresh and updated, making it ideal for pages that require up-to-date or dynamic content for every user request.
  • Client-Side Rendering (CSR): React runs JavaScript in the browser, generating the HTML and rendering it client-side, which gives you a high level of interactivity.


Server and Client Components: A Game Changer

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


// 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>
  );
};


Client-Side Rendering of Interactive UI


'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: Bridging Server and Client

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:


  1. React parses the server-rendered HTML and the serialized state sent from the server.
  2. It wires up event listeners, state, and interactive features to the existing DOM.
  3. The client React tree becomes "live," enabling dynamic updates without re-rendering static content.


Adding Event Handlers in Server Components

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;


Suspense: Optimizing the User Experience

React's Suspense API allows developers to defer rendering parts of the UI until the required data or code is ready.


Example with Suspense


const PostsPage = async () => {
  const posts = await fetchPosts();
  return (
    <Suspense fallback={<Spinner />}>
      <PostsList posts={posts} />
    </Suspense>
  );
};


In this example:

  • While the data is being fetched, a Spinner is displayed.
  • Once the server fetch is complete, the PostsList component renders the data seamlessly.


Conclusion

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.