Data Fetching, Server Actions, and Streaming in Next.js 15

Table of contents

Before we dive deep into the ways of fetching, updating, and handling data in the latest Next.js applications, let’s first cover some key concepts that will be used going forward.
⭐️ RSC (React Server Components) ?
React Server Components (RSC) are a feature in React that allows certain components to be rendered on the server rather than on the client. Unlike traditional server-side rendering (SSR), RSC enables components to fetch data and process logic without sending extra JavaScript to the browser, improving performance.
Key Features and Benefits of React Server Components (RSC)
✅ Runs on the Server → Reduces client-side JavaScript bundle size.
✅ Direct Data Fetching → Can fetch data from databases, APIs, or other backends without exposing credentials.
✅ Seamless Integration → Works alongside Client Components for interactive UI elements.
✅ Optimized Performance → Improves initial page load speed and reduces hydration costs.
Why Do We Need RSC?
🚀 Better Performance → Eliminates unnecessary client-side JavaScript execution.
🔍 Improved SEO → Pre-rendered content is easily indexed by search engines.
🛠 Simplified Client-Side Code → Fetching data in Server Components reduces state management complexity.
By leveraging RSC, Next.js enhances both performance and developer experience, making applications faster and more efficient. 🚀
How Next.js 15 Utilizes RSC ?
Next.js 15 fully embraces React Server Components in its App Router (app/
directory), making them the default. Here's how it leverages RSC:
1. Default Behavior in the App Router
In Next.js 15, components inside the app/
directory are Server Components by default.
This means:
You don’t need to use
useEffect
for data fetching.The component runs entirely on the server.
No extra JavaScript is sent to the browser.
2. Combining Server and Client Components
Some components, like interactive elements (useState
, useEffect
, event handlers), must run on the client. You can use the "use client"
directive for them.
3. Server Actions for Mutations
Next.js 15 extends RSC with Server Actions, allowing form submissions, mutations, and API calls without API routes.
How Are RSCs Different from Traditional React Components?
Feature | Server Components (RSC) | Client Components |
Runs On | Server | Browser |
Access to Backend | ✅ Yes | ❌ No |
JavaScript Sent to Client | Minimal | Full Component Code |
Use Cases | Data Fetching, Heavy Computation | User Interactions, State Management |
Well no worries , now we will dive into main part which is Data Fetching in NextJs and ways of it in both client and Server components where I’ll break things down and make it all click—promise ✨!
Fetching Data in Server Components
(Server Components can fetch data directly without useEffect
or client-side fetching)
Method 1: Making the Component Async
You can fetch data in Server components by turning your component into an asynchronous function, and await the fetch
call.
⦿ With the fetch
API
async function getData() {
const res = await fetch('https://api.example.com/data', { cache: 'no-store' });
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.message}</div>;
}
⦿ Fetching in a Separate Function and Awaiting in the Component
async function getData() {
const res = await fetch('https://api.example.com/data');
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.message}</div>;
}
⦿ With an ORM or database
You can fetch data with an ORM or database by turning your component into an asynchronous function, and awaiting the call:
import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Method 2 : Using Server Actions ( will cover separately for both client & server at end)
Fetching Data in Client Components
Method 1: Using the New use
Hook
'use client';
import { use } from 'react';
const getData = fetch('https://jsonplaceholder.typicode.com/posts/1').then((res)=>res.json());
export default function ClientComponent() {
const data = use(getData);
return <div>{data.message}</div>;
}
The use
hook is a new feature in React that simplifies working with async data fetching inside Server Components and Client Components. It was introduced to make handling Promises (like fetching API data or loading resources) more intuitive.🚀
what does use
do? Read more here
It suspends rendering until the Promise resolves.
It allows awaiting Promises inside components without using
useEffect
.It works in both Server and Client Components.
Method 2: Using useEffect
with useState
'use client';
import { useState, useEffect } from 'react';
export default function ClientComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []);
return <div>{data?.message || 'Loading...'}</div>;
}
Method 3: Using React Query for Better Caching. 🔗 Link
⭐️ Server Actions in Next.js?
Server Actions are asynchronous functions that are executed on the server. They can be called in Server and Client Components to handle form submissions and data mutations in Next.js applications.
Why Do We Need Server Actions?
Simplifies API Handling: Eliminates the need for writing separate API routes.
Better Security: Sensitive logic stays on the server and isn't exposed to the client.
Improved Performance: No extra API request overhead, reducing network latency.
Direct Database Access: Server Actions can interact with databases without exposing APIs.
Creating Server Functions
Convention
A Server Action can be defined with the React "use server"
directive. You can place the directive at the top of an async
function to mark the function as a Server Action, or at the top of a separate file to mark all exports of that file as Server Actions , or at the top of a separate file to mark all exports of that file.
// Create a server action for a specific task
'use server'
export async function createUser(formData: FormData) {
// Server-side logic for creating a user
}
export async function fetchUser(formData: FormData) {}
export async function deleteUser(formData: FormData) {}
In Server Components
Server Components can use the inline function level or module level "use server"
directive. To inline a Server Action, add "use server"
to the top of the function body:
export default function Page() {
// Server Action
async function create() {
'use server'
// Mutate data
}
return '...'
}
In Client Components
To call a Server Action in a Client Component, create a new file and add the "use server"
directive at the top of it. All exported functions within the file will be marked as Server Actions that can be reused in both Client and Server Components:
app/actions.ts
'use server'
export async function create() {}
app/button.tsx
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}
Server Actions in Next.js: Key Points
Invocation: Server Actions can be triggered through the
action
attribute in<form>
elements, and can also be invoked from event handlers,useEffect
, buttons, and third-party libraries.Progressive Enhancement: In Server Components, forms will submit even if JavaScript is disabled or hasn't loaded yet. In Client Components, submissions are queued until client hydration is complete.
Form Behavior: After hydration, form submissions won't cause a page refresh, ensuring a smoother user experience.
Integration with Next.js: Server Actions work with Next.js caching and revalidation, allowing UI updates and new data to be fetched in a single server roundtrip.
HTTP Method: Only the
POST
method is supported to invoke Server Actions.Serialization: The arguments and return values of Server Actions must be serializable by React.
Reusability: Server Actions are reusable functions, making them flexible for different parts of your application.
Best Practices for Using Server Actions Carefully
Avoid Overuse: Use them for form submissions and backend mutations, not for UI logic.
Sanitize Inputs: Prevent SQL injections and XSS attacks.
Handle Errors Properly: Ensure proper error handling using
try/catch
.
Note: In this post, we’ve touched on Server Actions, but we haven’t fully explored their use with form submissions. Stay tuned for my next article, where we’ll dive deeper into how to use Server Actions for form handling in Next.js 15.
Streaming in Next.js 15+
In Next.js 15, streaming is a feature that allows you to improve the initial load time and user experience by sending smaller chunks of a page's HTML from the server to the client progressively. This is especially useful when working with async/await in Server Components, which can lead to blocked routes due to slow data fetching.
How Streaming Works
Streaming can be implemented in two ways:
Using a
loading.js
fileUsing React's
<Suspense>
component
1. With loading.js
By adding a loading.js
file in the same folder as your page, you can display a loading state while data is being fetched. For instance, for a blog page, the file would look like this:
// app/blog/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
This loading.js
file will be wrapped inside layout.js
, automatically creating a <Suspense>
boundary around the page. As a result, users will see the layout and loading state immediately while the content is rendered.
2. With <Suspense>
To achieve more granular streaming, you can use React’s <Suspense>
component. By wrapping different parts of your page in <Suspense>
, you can control how each part is streamed, improving the user experience by showing content progressively.
Example:
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
export default function BlogPage() {
return (
<div>
{/* This content will be sent to the client immediately */}
<header>
<h1>Welcome to the Blog</h1>
<p>Read the latest posts below.</p>
</header>
<main>
{/* Any content wrapped in a <Suspense> boundary will be streamed */}
<Suspense fallback={<BlogListSkeleton />}>
<BlogList />
</Suspense>
</main>
</div>
)
}
In this case, the header is shown immediately, while the blog list content is streamed once it’s ready.
Creating Meaningful Loading States
For a smooth user experience, ensure your loading states are meaningful, such as skeleton screens, spinners, or preview content (e.g., a cover photo or title). This lets users know the app is responding.
You can inspect and preview these loading states during development with React DevTools.
By utilizing streaming, your Next.js application can deliver faster, more responsive experiences for users, especially when data fetching takes time.
Conclusion
By combining React Server Components and Server Actions, Next.js 15 delivers a seamless developer experience while boosting performance, security, and maintainability of your applications.
Subscribe to my newsletter
Read articles from Tarun directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tarun
Tarun
I have 3 year-experience as software developer skilled in ReactJS, TypeScript, NextJS, Redux.