DEV Community

Timevolt
Timevolt

Posted on

Next.js 14: Server Components – May the Code Be With You

The Quest Begins (The "Why")

Honestly, I was stuck in a loop. My Next.js 13 app felt like a sluggish hobbit trying to outrun a Nazgûl—every page load was a round‑trip to the server, fetching data, hydrating, and then re‑rendering on the client. Users complained about flash‑of‑unstyled‑content, and I was spending more time wrestling with getServerSideProps than actually building features. I kept asking myself: Is there a way to render UI on the server without sending the whole React tree to the browser?

That question led me to the treasure map hidden in the Next.js 14 release notes: Server Components. The promise was simple yet radical—write components that run exclusively on the server, never bundle their code for the client, and still compose them seamlessly with traditional Client Components. It felt like discovering a secret shortcut through Mordor that bypasses the whole mountain range. I had to try it, and boy, am I glad I did.

The Revelation (The Insight)

So what’s the magic? In Next.js 14, a file ending with .server.tsx (or .server.js) is treated as a Server Component by default. Anything imported only from that file never ends up in the client bundle. Meanwhile, a .client.tsx file (or any file that uses useState, useEffect, or React hooks) is a Client Component and gets sent to the browser. The beauty is that you can nest them: a Server Component can render a Client Component as a child, passing props down, but the server side does the heavy lifting—data fetching, markup generation, even accessing environment variables or databases—without exposing that code to the user.

Think of it like the Council of Elrond: the Server Component is the wise elf lord gathering knowledge (data) from the realms, while the Client Component is the brave hobbit that only needs to know what to do once the quest starts—no need to carry the whole library of lore.

The shift means:

  • Zero‑bundle cost for server‑only logic.
  • Streaming HTML—the server can start sending markup before data is fully ready, improving perceived performance.
  • Simpler data fetching—no more getServerSideProps or getStaticProps gymnastics; you can await a database call directly inside the component.

It felt like I’d just unlocked a new spell in my developer grimoire.

Wielding the Power (Code & Examples)

Let’s see the before‑and‑after. Imagine a simple blog page that fetches a post from a CMS and shows a comment form that needs client‑side state.

The Old Way (Next.js 13)

// pages/post/[slug].tsx
import { GetServerSideProps } from 'next';
import { useState } from 'react';

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { slug } = context.params;
  const post = await fetchPostFromCMS(slug);
  return { props: { post } };
};

export default function PostPage({ post }) {
  const [comment, setComment] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    await postComment(post.id, comment);
    setComment('');
  };

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.body }} />
      <form onSubmit={handleSubmit}>
        <textarea
          value={comment}
          onChange={(e) => setComment(e.target.value)}
          placeholder="Leave a comment"
        />
        <button type="submit">Post</button>
      </form>
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • The getServerSideProps function lives in the same file as UI logic, making the file harder to read.
  • The whole component (including the useState hook) is sent to the client, even though the data‑fetching part could stay on the server.
  • If we later add more server‑only utilities (like auth checks or server‑side caching), they bloat the client bundle unless we manually split them.

The New Way (Next.js 14 Server + Client Components)

// app/post/[slug]/page.server.tsx   ← Server Component
import { notFound } from 'next/navigation';
import CommentForm from './comment-form.client'; // Client Component

export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await fetchPostFromCMS(params.slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.body }} />
      {/* Client Component receives only the data it needs */}
      <CommentForm postId={post.id} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode
// app/post/[slug]/comment-form.client.tsx   ← Client Component
import { useState } from 'react';

interface Props {
  postId: string;
}

export default function CommentForm({ postId }: Props) {
  const [comment, setComment] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await postComment(postId, comment);
    setComment('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="Leave a comment"
      />
      <button type="submit">Post</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

What changed?

  1. Data fetching lives in the Server Component—no getServerSideProps needed, just plain await.
  2. The Server Component never ships its code to the client; only the minimal HTML (and any props passed to children) travels over the wire.
  3. The Client Component is tiny—it only contains the interactivity (useState, form handling). Its bundle size is now just a few hundred bytes instead of the whole page’s React tree.
  4. Clear separation of concerns: server‑only logic (fetching, auth, DB access) stays server‑side; UI state stays client‑side.

Traps to Avoid (The “Bosses” on Our Quest)

  • Accidentally importing a Client‑only hook inside a Server Component – Next.js will throw an error during build, but if you sneak it in via a dynamic import (() => import('./some-client-comp')) you’ll end up sending unnecessary code. Keep the import static and check the file extension (.client.*).
  • Passing large objects as props to a Client Component – Remember, props are serialized and sent to the client. If you pass the whole Post object (including huge body text), you’re blowing up the payload. Pass only what the client needs (e.g., postId).
  • Assuming server‑side environment variables are available in the client – Anything you import into a Server Component stays there. If you mistakenly reference process.env.NEXT_PUBLIC_* inside a Client Component that’s actually imported from a server file, you’ll get undefined. Keep env vars explicit: pass them as props if the client needs them.

Why This New Power Matters

Now that I’ve wielded Server Components, my pages load faster, my client bundles are slimmer, and my code feels cleaner—like I’ve finally organized my wizard’s tower after years of chaotic scrolls.

Imagine building a dashboard where the server fetches dozens of metrics, renders the heavy charts as static SVG, and only sends a tiny React widget for the date‑range picker. Or a commerce site where product data and pricing are rendered on the server, while the “Add to Cart” button stays interactive on the client. The possibilities feel as vast as the galaxy in Star Wars—except now the force is your code, and you’re the Jedi wielding it.

The biggest win? Less mental overhead. You no longer need to juggle getServerSideProps, getStaticProps, and client‑side data fetching strategies in parallel. You write async/await where it belongs, and the framework handles the rest.

Your Turn – The Challenge

I dare you to take a page in your own Next.js app that currently relies on getServerSideProps for data and a handful of client hooks for UI. Refactor it into a Server Component plus a minimal Client Component. Measure the bundle size before and after (thanks, next build and next-analytics or your favorite bundle‑visualizer). Share the results in the comments—let’s see who can shave off the most kilobytes!

May your components be light, your data be fresh, and your deployments be swift. Happy coding!

Top comments (0)