Learn React's server-side features: SSR with streaming, static pre-rendering, hydration, Server Components, and Server Functions. Discover how frameworks like Next.js enable modern full-stack React development.
Aurora Scharff
October 1, 2025
Modern React development has evolved far beyond single-page applications that render entirely in the browser. Today's React ecosystem includes powerful server-side capabilities that improve performance, SEO, and user experience through frameworks, server-side rendering, and innovative patterns like Server Components.
Let's explore how React frameworks handle the complexity of full-stack applications and examine the server-side features that make modern React development so powerful.
Building a production React app from scratch involves solving many complex problems: bundling, routing, server-side rendering, code splitting, and deployment optimization. React frameworks handle these concerns automatically, letting you focus on building features rather than configuring toolchains.
As the React documentation states: "If you want to build a new app or website with React, we recommend starting with a framework."
Next.js (App Router) - The most popular full-stack React framework that takes full advantage of React's architecture to enable full-stack React apps:
npx create-next-app@latest
React Router (v7) - The most popular routing library for React that can be paired with Vite to create a full-stack React framework, emphasizing standard Web APIs:
npx create-react-router@latest
TanStack Start - A full-stack React framework (currently in beta) powered by TanStack Router that provides full-document SSR, streaming, server functions, and bundling:
npx create-start-app@latest
Expo - A React framework that lets you create universal Android, iOS, and web apps with truly native UIs:
npx create-expo-app@latest
Each framework provides a complete development environment with optimized builds, automatic code splitting, and production-ready configurations out of the box.
Traditionally, websites generated HTML on the server for each request—this is how the web worked before single-page applications. React applications moved rendering to the browser, but this approach has limitations for SEO and initial page load performance. Server-side rendering (SSR) brings us back to generating HTML on the server for each request, improving initial page load times and enabling better SEO compared to client-side rendering, while still allowing for dynamic content.
Traditional SSR waits for the entire page to render before sending anything to the browser. Streaming SSR sends the initial shell immediately, then streams in content as it becomes available. Here's how React's renderToReadableStream
API works, though frameworks handle this automatically:
import { renderToReadableStream } from 'react-dom/server';
import { Suspense } from 'react';
async function handleRequest(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
}
function App() {
return (
<div>
<Header />
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
<Sidebar />
</div>
);
}
The browser receives the header and sidebar immediately, then posts content streams in as it becomes ready. Users see content faster, and slow components don't block the entire page.
SSR is particularly valuable when content needs to be personalized or updated frequently.
For content that doesn't change often, static pre-rendering generates HTML at build time rather than on each request. This provides the fastest possible loading times since the HTML is ready immediately, with no server processing needed.
Frameworks handle static pre-rendering through their build processes and routing configurations, using React's prerender
API under the hood. Developers configure this through framework-specific methods rather than calling the API directly.
Here's how the prerender
API works:
import { prerender } from 'react-dom/static';
async function generateStaticHTML() {
const { prelude } = await prerender(<BlogPost id="react-frameworks" />);
return prelude;
}
Different frameworks have various approaches to configuring static pre-rendering—some require explicit route configuration while others can make intelligent decisions automatically. Next.js, for example, can automatically determine when to statically pre-render pages based on their dependencies.
Static pre-rendering is ideal for content like blog posts, documentation, marketing pages, and product catalogs where the content is known at build time.
Hydration bridges server-rendered HTML and interactive React applications by attaching event listeners and state to existing markup. The hydrateRoot
API handles this process:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
const container = document.getElementById('root');
hydrateRoot(container, <App />);
Hydration requires the server and client to render identical content. Common mismatch causes include server/client environment differences, dynamic content like timestamps, and locale variations. React 19 improved hydration with better error reporting and automatic skipping of browser extension elements.
For unavoidable differences that would cause hydration mismatches, you can use suppressHydrationWarning
to prevent React from warning about specific elements. This example shows handling a timestamp that will differ between server and client:
function TimeComponent() {
return (
<p suppressHydrationWarning>
Current time: {new Date().toLocaleTimeString()}
</p>
);
}
Modern frameworks handle hydration automatically, but developers must ensure server and client render identically to avoid mismatches.
Server Components are a new type of component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server. This separate environment is the "server" in React Server Components.
In frameworks that support Server Components, they can run once at build time on your CI server, or they can be run for each request using a web server. They run exclusively on the server, contributing zero bytes to the client JavaScript bundle, and can directly access server-side resources like databases, file systems, and environment variables.
Here's an example of a Server Component:
async function BlogPost({ slug }) {
const post = await db.posts.findUnique({ where: { slug } });
const comments = await db.comments.findMany({ where: { postId: post.id } });
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<Comments comments={comments} />
<ShareButton url={`/posts/${post.slug}`} title={post.title} />
</article>
);
}
Notice how the BlogPost
Server Component can directly query the database, but when it needs interactive functionality like sharing, it renders a Client Component:
'use client';
function ShareButton({ url, title }) {
const handleShare = async () => {
if (navigator.share) {
await navigator.share({ title, url });
} else {
await navigator.clipboard.writeText(url);
alert('Link copied to clipboard!');
}
};
return (
<button onClick={handleShare}>
Share
</button>
);
}
This demonstrates the Server/Client Component boundary: Server Components handle data fetching and rendering, while Client Components handle browser interactions. Components need 'use client'
when they require client-side capabilities like:
onClick
, onSubmit
localStorage
, window
, document
useState
, useEffect
, and other React HooksServer Functions allow Client Components to call async functions executed on the server. When defined with the 'use server'
directive, frameworks that support Server Functions automatically create a reference to the Server Function and pass it to the Client Component. They provide type-safe mutations, form handling, and progressive enhancement.
Here's how to define Server Functions in a separate file:
// actions.js
'use server';
import { db } from './db';
export async function createPost(title, content) {
const post = await db.posts.create({
data: { title, content }
});
return { success: true, postId: post.id };
}
export async function deletePost(postId) {
await db.posts.delete({ where: { id: postId } });
}
Server Functions work with both programmatic calls and form actions. Always validate permissions and input since they run with full server privileges.
You can call Server Functions from Client Components in multiple ways:
'use client';
import { createPost, deletePost } from './actions';
function PostForm() {
const submitAction = async (formData) => {
const title = formData.get('title');
const content = formData.get('content');
await createPost(title, content);
};
return (
<form action={submitAction}>
<input name="title" placeholder="Post title" />
<textarea name="content" placeholder="Post content" />
<button type="submit">Create Post</button>
</form>
);
}
function DeleteButton({ postId }) {
return (
<form action={deletePost.bind(null, postId)}>
<button type="submit">Delete</button>
</form>
);
}
The examples above demonstrate both approaches for calling Server Functions:
Programmatic Calls: The PostForm
uses an async function that calls the Server Function programmatically. This gives you more control over the submission process, allowing you to handle loading states, validation, and user feedback before or after the server call.
Form Actions: The DeleteButton
uses the Server Function directly as a form action. This approach provides progressive enhancement—the form works even without JavaScript, making it ideal for critical functionality like data mutations.
The server-side features we've covered each solve specific challenges in modern web development:
These aren't just performance optimizations—they're architectural patterns that enable building applications that are fast by default, can work without JavaScript, and scale efficiently.
These server-side features work best when combined with React's concurrent features like useTransition
, Suspense
, and useOptimistic
. Together, they enable building applications that feel instant while handling complex server-client coordination seamlessly. For a deep dive into these concurrent features, see React Concurrent Features: An Overview.
React frameworks handle the complexity so you can focus on building features rather than configuring rendering strategies, automatically delivering fast, scalable applications that work well for both users and developers.
Sources:
Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.
React Frameworks and Server-Side Features: Beyond Client-Side Rendering
Learn React's server-side features: SSR with streaming, static pre-rendering, hydration, Server Components, and Server Functions. Discover how frameworks like Next.js enable modern full-stack React development.
Aurora Scharff
Oct 1, 2025
Promises in JavaScript: Receipts for Future Values and Advanced Patterns
JavaScript Promises manage future values with clean async handling. Learn how Promise.all, race, allSettled, and any simplify parallel tasks, timeouts, and error management.
Martin Ferret
Sep 30, 2025
Vue's Magical Reactivity Has Some Quirks
Why does Vue's reactivity sometimes behave unexpectedly? Explore ref unwrapping edge cases and cloning complications with clear examples and fixes.
Abdel Awad
Sep 30, 2025
We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.
Let us help you get the resources you need.