React 19 officially stabilizes features that were previously in experimental phase: React Server Components (RSC) and Server Actions. These components of the React architecture represent a significant paradigm shift in React application development, enabling a clear separation between code executed on the server and code executed on the client. Although these technologies have already been implemented in frameworks such as Next.js, React 19 officially incorporates them into the core library, stabilizing their public APIs while keeping the underlying implementations for frameworks and bundlers evolving.
The Evolution of Rendering in React
Before delving into RSCs, it's important to examine the evolution of rendering strategies in React:
Client-Side Rendering (CSR): React's original approach, where the server sends minimal HTML and the client performs all rendering, requiring significant JavaScript execution on the client.
Server-Side Rendering (SSR): Introduced to improve the initial user experience by shifting rendering from client to server. Instead of sending an empty HTML document, the server renders the initial HTML and sends it to the browser, reducing content display time.
Static Site Generation (SSG): This approach compiles and builds the entire application during the build phase, generating static files (HTML and CSS) that are then hosted on CDNs. It's particularly suitable for projects where content doesn't change frequently.
Incremental Static Regeneration (ISR): An evolution of SSG that sits between SSG and traditional SSR, allowing the regeneration of individual pages in response to a browser request, without the need to rebuild the entire site.
React Server Components represent the next step in this evolution, allowing developers to render some components entirely on the server, minimizing the JavaScript footprint on the client and offering a more granular approach to rendering.
React Server Components: Architecture and Functioning
React Server Components are components executed exclusively on the server, with several technical advantages over previous rendering strategies:
// ProductsPage.tsx
// Server Component (default setting in React 19)
import { db } from '../database';
import ProductCard from './ProductCard';
interface Product {
id: string;
name: string;
description: string;
price: number;
}
export default async function ProductsPage(): Promise {
// Direct access to database or other server-only resources
const products: Product[] = await db.products.findMany();
return (
Product Catalog
{products.map(product => (
))}
);
}
Key Technical Characteristics
Server-only execution: RSCs are executed exclusively on the server, both during build and during requests. They can operate once during the build phase on the CI server, or for each request using a web server.
Reduced client JavaScript: Client-side JavaScript bundles are significantly smaller because Server Components are not included in the client bundle. However, it's important to note that it's not completely "zero JavaScript" as client components that interact with Server Components still require JavaScript code.
Direct access to server resources: Components have access to all backend resources (database, filesystem, server, etc.), allowing queries to be sent directly from components, eliminating the need for intermediate API calls.
Integrated data management: Since components are rendered on the server, they can query the database directly. This shifts data loading to the server, significantly reducing latency compared to retrieving data from the client.
Usage limitations: Server Components don't have access to client-side event handlers, state, and effects. This means it's not possible to use event handlers or React hooks like useState, useReducer, and useEffect.
Differences from Traditional SSR
Unlike traditional Server-Side Rendering, React Server Components:
Eliminate the hydration process for server components: Server Components don't execute JavaScript on the client side and therefore don't require hydration as in SSR, where the entire page must be hydrated to become interactive.
Optimize JavaScript bundle size: As stated in the official RFC: "Server Components run only on the server and have zero impact on bundle size. Their code is never downloaded to clients, helping to reduce bundle sizes and improve startup time."
Allow direct access to server resources: Server Components offer direct access to server resources from within components, while with traditional SSR this is generally limited to the upper levels of the page.
Offer component-level granularity: Unlike SSR, which happens only once during the initial page load, Server Components can be individually reloaded from the server and incorporated into the existing component tree without losing client state.
This mixed architecture allows for a clearer separation between server rendering logic and client interactivity, enabling applications to combine the advantages of both approaches.
Composition of Client and Server Components
The React 19 architecture allows composition between server and client components:
// ServerComponent.tsx
// Server Component (default)
import ClientComponent from './ClientComponent';
export default function ServerComponent(): React.ReactElement {
// This code is executed only on the server
const serverData = fetchDataFromDatabase();
return (
Data from server: {serverData}
{/* Client components can be nested in server components */}
);
}
// ClientComponent.tsx
'use client';
import { useState } from 'react';
interface ClientComponentProps {
initialData: string;
}
export default function ClientComponent({ initialData }: ClientComponentProps): React.ReactElement {
// This code is executed on the client
const [data, setData] = useState(initialData);
return (
Data on client: {data}
);
}
This pattern allows:
Executing data access logic and initial rendering on the server
Maintaining interactivity on the client where needed
Optimizing bundle size by sending JavaScript only for client components
Server Actions: Server Functions Callable from the Client
Server Actions complement Server Components, providing a mechanism to execute code on the server in response to events on the client:
// actions.ts
'use server';
import { z } from 'zod';
import { db } from '../database';
const ProductSchema = z.object({
// schema definition
});
type ProductData = z.infer;
export async function createProduct(formData: FormData): Promise<{
success: boolean;
error?: string | Record;
}> {
try {
const rawData = {
name: formData.get('name'),
price: formData.get('price'),
description: formData.get('description') || '',
};
// Validation with Zod
const validationResult = ProductSchema.safeParse(rawData);
// If validation fails, return errors
if (!validationResult.success) {
// Format Zod errors
const formattedErrors = validationResult.error.format();
return {
success: false,
error: formattedErrors
};
}
// Get validated data
const data = validationResult.data;
// Direct database access
await db.products.create({
data
});
return { success: true };
} catch (error) {
console.error('Error creating product:', error);
return {
success: false,
error: 'An error occurred while creating the product'
};
}
}
Implementation in Client Component
// ProductForm.tsx
'use client';
import { useActionState } from 'react';
import { createProduct, ActionResponse } from './actions';
import { useState } from 'react';
// Initial state
const initialState: ActionResponse | null = null;
export default function ProductForm(): React.ReactElement {
// Local state for validation errors
const [fieldErrors, setFieldErrors] = useState>({});
// useActionState to manage the entire action
const [state, formAction, isPending] = useActionState(
async (previousState, formData) => {
try {
// Call the Server Action
const result = await createProduct(formData);
// Update field errors when necessary
if (!result.success && result.errors) {
setFieldErrors(result.errors);
} else {
// Reset errors when action succeeds
setFieldErrors({});
if (result.success) {
setTimeout(() => {
document.querySelector('form')?.reset();
}, 0);
}
}
return result;
} catch (error) {
return {
success: false,
message: 'An error occurred during the request'
};
}
},
initialState
);
return (
Add new product
);
}
Technical Advantages of Server Actions
API endpoint simplification: Server Actions eliminate the need to create API endpoints to modify data. They allow you to write asynchronous functions that execute on the server and can be invoked directly from Client or Server Components.
Native form integration: By passing a Server Function to a form's action attribute, React can progressively enhance the form, allowing submission even before the JavaScript bundle is fully loaded.
End-to-end typing: Unlike traditional API endpoints, Server Actions are inherently type-safe, ensuring greater consistency between client and server.
UI state management: With new hooks like useActionState and useFormStatus, Server Actions offer integrated mechanisms for managing loading states and errors during form operations.
These features make Server Actions particularly suitable for applications that require strong integration between client and server, while maintaining a clear separation of responsibilities.
Conclusions
React Server Components and Server Actions represent a paradigm shift in React application development, promoting a clearer separation between server and client logic. These features, now stabilized in React 19, offer significant advantages in terms of performance, security, and code complexity.
Adopting these technologies requires rethinking application architecture, but offers substantial benefits that justify the investment, especially for applications with high performance and SEO requirements.