React Server Components e Server Actions: La Rivoluzione di React 19 per Applicazioni Moderne

Published: May 5, 2025

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.

  2. 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."

  3. 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.

  4. 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:

  1. Executing data access logic and initial rendering on the server

  2. Maintaining interactivity on the client where needed

  3. 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

{fieldErrors.name && (
{fieldErrors.name.map((error, i) => (

{error}

))}
)}
{fieldErrors.price && (
{fieldErrors.price.map((error, i) => (

{error}

))}
)}