≡ Menu

React Server Components (RSC) have redefined modern web development, offering massive performance gains by shifting rendering and data fetching from the browser to the server. If you’re using the Next.js App Router, you’re already using RSC!

This post walks you through building a simple, highly performant blog using Next.js Server Components for data fetching and rendering, coupled with Client Components only where true interactivity is needed.


The Power Duo: Server Components (Default) and Client Components ('use client')

In Next.js App Router, every component is a Server Component by default. This is the key to performance, as these components:

  1. Can fetch data directly (using async/await).

  2. Have access to server-side resources (like environment variables).

  3. Do not ship their code to the client’s JavaScript bundle.

We use Client Components only for features requiring browser APIs, state, or event handlers (like onClick).

1. 📂 Mocking Server-Side Data (lib/posts.ts)

In a real-world scenario, the function below would contain your database query (e.g., using Prisma or an ORM). Here, we simulate that server-side access and a network delay.

// src/lib/posts.ts

export interface Post {
  id: string;
  title: string;
  content: string;
  author: string;
  date: string;
}

export async function getPosts(): Promise<Post[]> {
  // Simulate network delay and database query
  await new Promise((resolve) => setTimeout(resolve, 1000));

  return [
    /* ... mock data objects ... */
    // { id: '1', title: '...', content: '...', author: '...' },
    // { id: '2', title: '...', content: '...', author: '...' },
  ];
}

By placing this code on the server, we ensure that secrets or connection strings used within getPosts are never exposed to the client.

2. ❤️ The Interactive Client Component (components/LikeButton.tsx)

This component handles user interaction (the button click) and manages local state (the like count). Because it needs browser capabilities, we mark it with the 'use client' directive.

// src/components/LikeButton.tsx
'use client'; 

import { useState } from 'react';

// ... component definition ...

export default function LikeButton({ initialLikes }: LikeButtonProps) {
  const [likes, setLikes] = useState(initialLikes);

  const handleClick = () => {
    setLikes((prevLikes) => prevLikes + 1);
    // Real-world: Send update to a Server Action or API endpoint
  };

  return (
    <button
      onClick={handleClick}
      // ... Tailwind CSS classes ...
    >
      ❤️ Like ({likes})
    </button>
  );
}

Key Takeaway: The Client Component file is the only one whose code is sent to the browser.

3. 📝 The Rendering Server Component (components/BlogPost.tsx)

This component receives the post data (fetched on the server) and structures the UI. It remains a Server Component, even though it renders the LikeButton Client Component inside it.

// src/components/BlogPost.tsx

import { Post } from '@/lib/posts';
import LikeButton from './LikeButton'; // Importing a Client Component

// ... component definition ...

export default function BlogPost({ post }: BlogPostProps) {
  return (
    <article className="bg-white p-6 rounded-lg shadow-md mb-8">
      <h2 className="text-3xl font-bold text-gray-800 mb-2">{post.title}</h2>
      {/* ... other post details ... */}
      <div className="prose prose-lg text-gray-700">
        <p>{post.content}</p>
      </div>
      
      {/* Renders the static HTML of the LikeButton, then hydrates it */}
      <LikeButton initialLikes={Math.floor(Math.random() * 100)} /> 
    </article>
  );
}

4. 🌐 The Main Page Component (app/page.tsx)

The main page is where the entire process starts. Since it is a Server Component, we can make it an async function and call our server-side data fetching utility directly.

// src/app/page.tsx

import { getPosts } from '@/lib/posts';
import BlogPost from '@/components/BlogPost';

// The 'async' keyword allows direct server-side data fetching
export default async function HomePage() {
  const posts = await getPosts(); // Data fetched BEFORE the component renders

  return (
    <main className="container mx-auto px-4 py-8 max-w-3xl">
      <h1 className="text-5xl font-extrabold text-center text-gray-900 mb-12">
        My Awesome RSC Blog
      </h1>
      <section>
        {posts.map((post) => (
          <BlogPost key={post.id} post={post} />
        ))}
      </section>
    </main>
  );
}

Conclusion: Performance by Default

By using Next.js Server Components, we achieve three major benefits:

  1. Zero-Bundle Static Content: The vast majority of the blog post text is rendered to HTML on the server, avoiding client-side JavaScript download for the content.

  2. Efficient Data Fetching: Data retrieval happens close to the source (the server), eliminating the need for client-side API roundtrips and complex state management (useEffect, loaders).

  3. Clean Separation of Concerns: We clearly separate server logic (data, secrets) from client logic (state, events), leading to cleaner, more maintainable code.

This pattern—Server Components for content and data, Client Components for interaction—is the blueprint for building performant, modern web applications.

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 1 comment }

This tutorial demonstrates the ideal use case for the experimental useEffectEvent hook: creating stable, non-reactive callbacks that access fresh state or props without forcing your primary useEffect hook to re-run.

The Goal: Stable Auto-Saving

We want an editor component that automatically saves its content to an API every 5 seconds.

  • Requirement 1 (Stability): The 5-second timer/interval must be set up only once when the component mounts. It should not reset every time the user types.

  • Requirement 2 (Freshness): The save function must always read the latest text the user has typed (the latest content state).

Step-by-Step Implementation

Step 0: Setup and API Mock

We start with the basic component structure and mock the API function that handles the saving logic.

import { useEffect, useState, useEffectEvent } from 'react';

// Mock API function for demonstration
const api = { 
  save: (data) => console.log('Saving latest content:', data.substring(0, 20) + '...') 
};

function DocumentEditor() {
  const [content, setContent] = useState('');
  // ... (Steps 1 & 2 will go here)
  
  return (
    <div>
      <h3>Document Editor (Saves every 5s)</h3>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows="10"
        cols="50"
        placeholder="Start typing..."
      />
    </div>
  );
}

Step 1: Define the Effect Event (The Non-Reactive Action)

The core saving logic is defined here. By wrapping it in useEffectEvent, we ensure that the function doSave always has a stable identity across re-renders, yet the code inside (which uses content) always sees the latest state.

// 1. Define the Effect Event (The Stable Function)
const doSave = useEffectEvent(() => {
  //  This function ALWAYS sees the LATEST 'content' state.
  if (content.trim() !== '') {
    api.save(content);
  }
});
  • Benefit: This function can be safely called from inside useEffect without being listed as a dependency.


Step 2: Define the Effect (The Reactive Setup)

Now we set up the interval using a standard useEffect. Since the function we call (doSave) is stable, the dependency array remains empty, fulfilling our stability requirement.

// 2. Define the Effect (The Setup Logic)
useEffect(() => {
  console.log('--- Setting up auto-save interval ---');
  
  // The interval calls the stable 'doSave' event
  const intervalId = setInterval(() => {
    doSave(); // Calls the stable event which reads the fresh 'content'
  }, 5000);

  // Cleanup: Clears the interval when the component unmounts or effect re-runs
  return () => {
    console.log('--- Clearing auto-save interval ---');
    clearInterval(intervalId);
  };
}, []); //  Dependency Array: Empty means it only runs ONCE on mount.
  • Result: The useEffect sets the timer once. Typing into the textarea updates content, but it does not trigger the useEffect to re-run and reset the timer.

Every 5 seconds, the stable doSave function runs and accesses the current, fresh content.

Summary of the Complete Component:

import { useEffect, useState, useEffectEvent } from 'react';

const api = { 
  save: (data) => console.log('Saving latest content:', data.substring(0, 20) + '...') 
};

function DocumentEditor() {
  const [content, setContent] = useState('');
  
  // 1. STABLE ACTION: This reads fresh state (content) but has a fixed identity.
  const doSave = useEffectEvent(() => {
    if (content.trim() !== '') {
      api.save(content);
    }
  });

  // 2. STABLE SETUP: This sets up the non-restarting timer.
  useEffect(() => {
    console.log('--- Setting up auto-save interval ---');
    const intervalId = setInterval(() => {
      doSave(); // Calls the stable event
    }, 5000);

    return () => {
      console.log('--- Clearing auto-save interval ---');
      clearInterval(intervalId);
    };
  }, []); // Only runs once.

  return (
    <div>
      <h3>Document Editor (Saves every 5s)</h3>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows="10"
        cols="50"
        placeholder="Start typing..."
      />
    </div>
  );
}

Have any questions about the code above?

Leave them in the comments section below!

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments }