Server Components in NextJS 14

Isaac Okoro
August 16, 2024|
5 min read
abstract art

Keeping Interactive States Elevated: A tutorial on utilizing server components in NextJS 14

 

Server components are a feature that allows you to create React UI components that can render on the server side. This approach differs from traditional client-side rendering, where components are rendered in the user's browser. Server components enable you to offload the rendering of specific application parts to the server, providing performance, state management, and code-splitting benefits.

Server components in Next 14 come with many features, but perhaps most notable is that it implements different server rendering strategies like Static Rendering, which is the default, Dynamic Rendering, and Streaming.

 

Benefits of Server components

This section will explore certain benefits that come with Server Components in Next js

 

Caching

Server components get rendered on the server, meaning the results can be cached effectively and reused on subsequent requests. This can lead to cost reduction by reducing the amount of data fetching required and can, in turn, increase app performance.

 

 

Optimized Data Fetching

Server components allow you to optimize your applications by moving your data fetching to the server, which is inherently closer to your data source. This shift to the server can improve the performance of your application by reducing the time it takes for data fetching.

 

Enhanced Security

Security is always a concern when creating software, and server components provide an excellent solution to this concern. Sensitive data and logic can be kept on the server without the risk of exposing them to client-side code. By keeping sensitive data on the server with server components, you provide your application with an added layer of security while creating a great UI experience.

 

Implementing interactive state with server components

 

When I started with server components, one question came to mind: How do I enable dynamic user interactions and efficient state management with server components?

 

You can't use the state hook (or any hook for that matter) when using server components.

The solution is to make use of strategically placed client components. These client components take care of your application's stateful logic and event handling while co-existing with the server-rendered side of your code. This is known as interleaving server and client components.

 

Here is an example: Say you have an application with a logo, sidebar, navigation, and search. You can make the whole page a server component and then make the search a client component because the search component will most likely have an input that autocompletes search results.

Note that there is a restriction when interleaving server components and client components.

A Client component cannot import a Server component. If this happens, then the server component automatically becomes a Client component.

 

The below example is wrong:

 

"use client";

import ServerComp from "./ServerComponent";

export default function ClientComp() {

 return (

 <div>

  <h1>The below component is a Server Component</h1>

  <ServerComp />

 </div>

 );

}

 

The solution is to pass the Server component as a child prop of the Client component and then wrap the two components in another Server component. Below is an example:

 

"use client";

export default function ClientComp({ children }) {

 return (

 <div>

  <h1>The below component is a Server Component</h1>

  {children}

 </div>

 );

}

 

In the above example, a Client component imports and renders the children prop. Now, we can wrap the components in another Server component, as shown below:

 

import ClientComp from "./ClientComp";

import ServerComp from "./ServerComponent";

export default function Home() {

 return (

 <>

  <ClientComp>

   <ServerComp />

  </ClientComp>

 </>

 );

}

 

We imported both the client component we created and the server component we want to use in the home component, which is a server component. We then wrap the imported Server component into the Client component.

 

With this pattern, the server component will be rendered on the server, establishing clear boundaries with the Client component.

 

Fetching Data in Server components

Data fetching is a vital part of every application. In Next js, the fetch Web API has been extended to handle caching and revalidation for every fetch request on the server.

 

You can use the fetch API with async/await in Server Components; some common mistakes when fetching data in server components are using multiple network requests for one fetch and hardcoding absolute URLs in your code. Let's look at an example below:

 

Say we have a Route Handler in the API folder in our application, as shown below:

 

// app/api/hello/route.js

import { NextResponse } from "next/server";

export async function GET() {

 return NextResponse.json({ data: "Hello world" });

}

 

Now, the request for the data will look something like this:

 

export default async function Page() {

 let response = await fetch(process.env,NEXT_PUBLIC_BASE_URL);

 let data = await response.json();

 return <h1>{JSON.stringify(data)}</h1>;

}

 

While the code above is correct, it outlines the mistakes we mentioned earlier—the Route Handler and the server component run on the server, which causes the server component to also call the same server for its data. In another server framework, this would be akin to calling an endpoint from another endpoint instead of just calling the helper directly from the first endpoint. We also had to hardcode the URL to be able to make requests from the Route Handler.

 

Instead, we can access the server data directly in the server component as shown below:

 

import { NextResponse } from "next/server";

async function sayHello() {

 return NextResponse.json({ data: "Hello world" });

}

export default async function Page() {

 //You can do this with internal APIs

 let res = await sayHello();

 //You can also call an external API like so:

 let fetchRes = await fetch("http://localhost:3000/api/hello");

 // ...

}

 

In the code above, we directly called the function in the server component, resolving the issues we had previously.

 

React Suspense with Server Components

React Suspense enhances the user experience by offering alternate content to be shown until any asynchronous operation is resolved, and this is effectively handling interactive loading state on the server side. The alternate content can be a skeleton state or loading spinner.

 

An example of using React Suspension with server component:

 

import { Suspense } from 'react';

async function PostTitle() {

 let data = await fetch('https://jsonplaceholder.typicode.com/posts');

 let posts = await data.json();

 return (

 <ul>

  {posts.map((post) => (

   <li key={post.id}>{post.title}</li>

  ))}

 </ul>

 );

}

export default function Page() {

 return (

 <section>

  <h1>Blog Posts</h1>

  <Suspense fallback={<p>Loading...</p>}>

   <PostTitle />

  </Suspense>

 </section>

 );

}

 

Note: The Suspense boundary must be placed higher than the async component doing the fetching.

 

The above example shows one way of using React Suspense with Server Components. Another way is to create a loading.js file in the folder where you are doing any data fetching. This file can hold any meaningful loading state, like skeleton screens or a loading spinner. Now Next.js will automatically use the loading.js file when fetching starts and immediately swap the file with the new content when fetching is done and rendering is complete.

 

Let's put it all together and build a sample application showcasing how to use Server Components. In this example, we want as much of our application to remain on the server and so we will be interleaving our components.

 

First, we create our client component as shown on the code block below:

 

"use client";

import { useState } from "react";

export default function Client({ children }) {

 const [showContent, setShowContent] = useState(false);

 const pageStyle = {

   backgroundColor: "black",

   minHeight: "100vh",

   display: "flex",

   justifyContent: "center",

   alignItems: "center",

   color: "white",

 };

 const buttonStyle = {

   padding: "8px",

   backgroundColor: "#BC359C",

   color: "white",

   cursor: "pointer",

   border: "none",

   fontSize: "1.5rem",

   borderRadius: "6px",

   marginBottom: "10px",

 };

 const contentStyle = {

   padding: "5px",

   display: 'block',

   transition: "opacity 0.5s ease-in-out",

   opacity: showContent ? 1 : 0,

 };

 return (

   <div style={pageStyle}>

     <div style={{ textAlign: "center" }}>

       <button

         onClick={() => setShowContent(!showContent)}

         style={buttonStyle}

       >

         Toggle Content

       </button>

       <div style={contentStyle}>{showContent && <div>{children}</div>}</div>

     </div>

   </div>

 );

}

 

In the code block above, we have a component that toggles some content. Ideally we want this content to come from the server. With that in mind, we want the button to toggle the state of showing the server content.

 

In the index file of our application, we can now import our component and interleave them as shown below:

 

import ServerComponent from "./ServerComponent";

import Client from "./Client";

export default function Home() {

 return (

   <>

     <Client>

       <ServerComponent />

     </Client>

   </>

 );

}

 

With that done, our application should look like the image below:


toggle button


With that done, let's work on the server component.

 

import { Suspense } from "react";

import Card from "./component/Card";

async function PostTitle() {

 const data = await fetch("https://dummyjson.com/users");

 const postsResponse = await data.json();

 const posts = postsResponse.users;

 

 const gridContainerStyle = {

   display: "grid",

   gridTemplateColumns: "repeat(3, 1fr);",

   gap: "20px",

   gridAutoRows: "minmax(100px, auto);",

 };

 return (

   <>

     <div style={gridContainerStyle}>

       {posts.map((p) => (

         <Card

           key={p.id}

           image={p.image}

           firstName={p.firstName}

           lastName={p.lastName}

           email={p.email}

         />

       ))}

     </div>

   </>

 );

}

export default function ServerComponent() {

 return (

   <section>

     <h1>User Cards</h1>

     <Suspense fallback={<p>Loading...</p>}>

       <PostTitle />

     </Suspense>

   </section>

 );

}

 

In the code block above, we fetch data from a free API and then mapped the information we needed to a Card component created for that purpose and you can find the code for the Card component below:

 

import Image from "next/image";

export default function Card({ image, firstName, lastName, email }) {

 const cardStyle = {

   width: "300px",

   border: "1px solid #ddd",

   borderRadius: "8px",

   overflow: "hidden",

   marginBottom: "20px",

   padding: "5px",

 };

 const nameStyle = {

   fontSize: "18px",

   fontWeight: "bold",

   margin: "8px 0",

 };

 const emailStyle = {

   color: "#666",

   margin: "8px 0",

 };

 return (

   <div style={cardStyle}>

     <Image src={image} width={100} height={100} alt="img" />

     <div

       style={{

         textAlign: "left",

         padding: "10px 0 10px 15px",

       }}

     >

       <div style={nameStyle}>

         Name: {firstName} {lastName}

       </div>

       <div style={emailStyle}>Email: {email}</div>

     </div>

   </div>

 );

}

 

With that done, we navigate to the browser and when we click on the button to toggle the content we should see our results as shown in the image below:


toggle button


This example demonstrates how to keep as much of our application on the server as possible and only the barest minimum on the client. The complete code used in this tutorial can be found here.

 

Best practices:

 

Design higher-order components (HOCs) that work seamlessly on the server and client using environment-agnostic HOCs. Use checks to ensure compatibility with server-side execution.

 

Modular Code Organization:

Organize your code into feature-centric modules. Maintain the server and client components, assets, tests, and styles of each feature together.

 

Keep Server components pure

Keep server components pure and free of client state for best performance and keep client interactivity as low in the stack as possible

 

Conclusion

This article has examined Server Components in Next js. We have examined their benefits, how to elevate the interactive state by interleaving server and client components, and best practices when using them.