Saltar al contenido principal
Volver al blog

React Server Components: guía práctica con Next.js

Ray MartínRay Martín
9 min de lectura
React Server Components: guía práctica con Next.js

Qué son los React Server Components y por qué importan

Los React Server Components (RSC) representan el cambio más significativo en la arquitectura de React desde la introducción de los hooks. Son componentes que se ejecutan exclusivamente en el servidor: su código nunca se envía al navegador, lo que significa cero kilobytes de JavaScript en el bundle del cliente para esos componentes.

En un modelo tradicional de React, todo el código de tus componentes — incluyendo lógica de transformación de datos, librerías de formateo, validaciones — se envía al cliente aunque solo se ejecute una vez durante el renderizado inicial. Con Server Components, ese código permanece en el servidor y solo el HTML resultante llega al navegador.

typescript
// Este componente es un Server Component por defecto en Next.js 15
// Su código NUNCA se envía al navegador
import { formatDistance } from "date-fns";
import { es } from "date-fns/locale";

interface Article {
  id: string;
  title: string;
  content: string;
  createdAt: Date;
}

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // Fetch directamente en el componente — sin useEffect, sin useState
  const article: Article = await fetch(
    `https://api.example.com/articles/${id}`,
    { next: { revalidate: 3600 } }
  ).then((res) => res.json());

  const timeAgo = formatDistance(new Date(article.createdAt), new Date(), {
    addSuffix: true,
    locale: es,
  });

  return (
    <article className="prose prose-lg max-w-3xl mx-auto">
      <h1>{article.title}</h1>
      <time className="text-gray-500">{timeAgo}</time>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  );
}

En este ejemplo, la librería date-fns (que pesa más de 70KB) nunca se incluye en el bundle del cliente. El servidor ejecuta el formateo, genera el HTML y envía solo el resultado. El usuario recibe una página más ligera y rápida.

Server vs Client Components: cuándo usar cada uno

La decisión entre Server y Client Component depende de lo que necesita hacer el componente. Esta es la regla general:

  • Server Component (por defecto en Next.js 15): Para todo lo que no requiere interactividad del usuario ni APIs del navegador.
  • Client Component (con "use client"): Para interactividad, estado, efectos y APIs del navegador.

Cuándo usar Server Components

  1. Fetching de datos: Acceso directo a bases de datos, APIs o sistemas de archivos.
  2. Renderizado de contenido: Markdown, HTML, listas estáticas.
  3. Lógica de negocio pesada: Transformaciones de datos, cálculos, formateo.
  4. Acceso a secretos: Variables de entorno del servidor, API keys, tokens.
  5. Componentes de layout: Headers, footers, sidebars sin interactividad.

Cuándo usar Client Components

  1. Interactividad: onClick, onChange, onSubmit y otros event handlers.
  2. Estado local: useState, useReducer.
  3. Efectos: useEffect, useLayoutEffect.
  4. APIs del navegador: localStorage, window, navigator, IntersectionObserver.
  5. Hooks personalizados: Cualquier hook que use estado o efectos.
  6. Librerías de terceros con estado: Formularios (react-hook-form), animaciones (framer-motion).
typescript
// Tabla de decisión rápida
// ┌─────────────────────────────┬────────┬────────┐
// │ Necesidad                   │ Server │ Client │
// ├─────────────────────────────┼────────┼────────┤
// │ Fetch de datos              │   ✓    │        │
// │ Acceso a backend directo    │   ✓    │        │
// │ Secretos del servidor       │   ✓    │        │
// │ Dependencias pesadas        │   ✓    │        │
// │ onClick / onChange          │        │   ✓    │
// │ useState / useReducer       │        │   ✓    │
// │ useEffect                   │        │   ✓    │
// │ APIs del navegador          │        │   ✓    │
// │ Context providers           │        │   ✓    │
// │ Class-based components      │        │   ✓    │
// └─────────────────────────────┴────────┴────────┘

La directiva "use client" y sus límites

La directiva "use client" se coloca al inicio de un archivo para marcar ese módulo (y todo lo que importa) como código que se ejecuta en el cliente. Es una frontera: todo lo que esté por debajo de esa directiva en el árbol de importaciones se convierte en parte del bundle del cliente.

typescript
// components/Counter.tsx
"use client"; // Esta línea marca la frontera servidor-cliente

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div className="flex items-center gap-4">
      <button
        onClick={() => setCount((c) => c - 1)}
        className="rounded-lg bg-gray-200 px-4 py-2 hover:bg-gray-300"
        aria-label="Decrementar"
      >
        -
      </button>
      <span className="text-2xl font-bold tabular-nums">{count}</span>
      <button
        onClick={() => setCount((c) => c + 1)}
        className="rounded-lg bg-primary-600 px-4 py-2 text-white hover:bg-primary-700"
        aria-label="Incrementar"
      >
        +
      </button>
    </div>
  );
}

Puntos importantes sobre la directiva:

  • Es una frontera de módulo: Afecta al archivo donde se declara y a todos los módulos que ese archivo importa. No puedes tener un Server Component importando directamente desde un archivo marcado con "use client" y esperar que se ejecute en el servidor.
  • No "infecta" hacia arriba: Un Server Component puede renderizar un Client Component como hijo. La directiva solo afecta hacia abajo en el árbol de importaciones.
  • Debe ser la primera instrucción: La directiva debe ser literalmente la primera línea del archivo (excluyendo comentarios).

Error común: Poner "use client" en un archivo de layout o en un componente padre de alto nivel. Esto convierte toda la rama en Client Components, perdiendo todos los beneficios de RSC. La directiva debe estar lo más abajo posible en el árbol de componentes.

Fetching de datos en Server Components

Una de las ventajas más poderosas de los Server Components es que pueden ser funciones async. Puedes usar await directamente en el componente sin necesidad de useEffect, useState ni librerías de data fetching.

typescript
// app/[locale]/projects/page.tsx
// Este componente es async — se ejecuta en el servidor
interface Project {
  id: string;
  title: string;
  description: string;
  stack: string[];
  url: string;
}

async function getProjects(): Promise<Project[]> {
  const res = await fetch("https://api.example.com/projects", {
    next: { revalidate: 3600 }, // Revalidar cada hora
  });

  if (!res.ok) throw new Error("Error al obtener proyectos");
  return res.json();
}

export default async function ProjectsPage() {
  const projects = await getProjects();

  return (
    <main className="container mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">Proyectos</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {projects.map((project) => (
          <article
            key={project.id}
            className="rounded-xl border border-gray-200 p-6 hover:shadow-lg transition-shadow"
          >
            <h2 className="text-xl font-semibold">{project.title}</h2>
            <p className="mt-2 text-gray-600">{project.description}</p>
            <div className="mt-4 flex flex-wrap gap-2">
              {project.stack.map((tech) => (
                <span
                  key={tech}
                  className="rounded-full bg-primary-100 px-3 py-1 text-xs font-medium text-primary-800"
                >
                  {tech}
                </span>
              ))}
            </div>
          </article>
        ))}
      </div>
    </main>
  );
}

Acceso directo a la base de datos

Los Server Components pueden acceder directamente a bases de datos sin exponer credenciales ni crear endpoints API intermedios:

typescript
// app/[locale]/blog/[slug]/page.tsx
import { db } from "@/lib/database";
import { notFound } from "next/navigation";

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  const post = await db.post.findUnique({
    where: { slug, published: true },
    include: {
      author: { select: { name: true, avatar: true } },
      tags: true,
    },
  });

  if (!post) notFound();

  return (
    <article className="prose prose-lg max-w-3xl mx-auto py-12">
      <header>
        <h1>{post.title}</h1>
        <div className="flex items-center gap-3 not-prose">
          <img
            src={post.author.avatar}
            alt={post.author.name}
            className="h-10 w-10 rounded-full"
          />
          <span className="text-gray-600">{post.author.name}</span>
        </div>
      </header>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Patrones de composición: padre servidor con hijos cliente

El patrón más potente de los RSC es la composición: un Server Component puede renderizar Client Components como hijos, pasándoles datos a través de props. Esto permite mantener la lógica de fetching en el servidor mientras la interactividad se maneja en el cliente.

typescript
// app/[locale]/dashboard/page.tsx (Server Component)
import { db } from "@/lib/database";
import { DashboardChart } from "@/components/dashboard/DashboardChart";
import { DashboardFilters } from "@/components/dashboard/DashboardFilters";
import { StatsCards } from "@/components/dashboard/StatsCards";

export default async function DashboardPage() {
  // Fetch de datos en el servidor
  const [stats, chartData, recentActivity] = await Promise.all([
    db.analytics.getStats(),
    db.analytics.getChartData({ period: "30d" }),
    db.activity.getRecent({ limit: 10 }),
  ]);

  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Dashboard</h1>

      {/* StatsCards es un Server Component — solo HTML */}
      <StatsCards stats={stats} />

      {/* DashboardFilters es un Client Component — tiene estado */}
      <DashboardFilters />

      {/* DashboardChart es un Client Component — usa una librería de gráficos */}
      <DashboardChart data={chartData} />

      {/* La lista es un Server Component — sin interactividad */}
      <section className="mt-8">
        <h2 className="text-xl font-semibold mb-4">Actividad reciente</h2>
        <ul className="space-y-3">
          {recentActivity.map((item) => (
            <li key={item.id} className="rounded-lg border p-4">
              <p className="font-medium">{item.description}</p>
              <time className="text-sm text-gray-500">{item.timestamp}</time>
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}
typescript
// components/dashboard/DashboardChart.tsx
"use client";

import { useState } from "react";

interface ChartDataPoint {
  date: string;
  value: number;
}

interface DashboardChartProps {
  data: ChartDataPoint[];
}

export function DashboardChart({ data }: DashboardChartProps) {
  const [period, setPeriod] = useState<"7d" | "30d" | "90d">("30d");

  // Los datos iniciales vienen del servidor via props
  // El estado local maneja la interactividad del cliente
  const filteredData = data.filter((point) => {
    const daysAgo = period === "7d" ? 7 : period === "30d" ? 30 : 90;
    const cutoff = new Date();
    cutoff.setDate(cutoff.getDate() - daysAgo);
    return new Date(point.date) >= cutoff;
  });

  return (
    <div className="rounded-xl border p-6">
      <div className="flex gap-2 mb-4">
        {(["7d", "30d", "90d"] as const).map((p) => (
          <button
            key={p}
            onClick={() => setPeriod(p)}
            className={
              period === p
                ? "bg-primary-600 text-white rounded-lg px-3 py-1"
                : "bg-gray-100 rounded-lg px-3 py-1 hover:bg-gray-200"
            }
          >
            {p}
          </button>
        ))}
      </div>
      {/* Renderizar el gráfico con los datos filtrados */}
      <div className="h-64 flex items-end gap-1">
        {filteredData.map((point) => (
          <div
            key={point.date}
            className="bg-primary-500 rounded-t flex-1 min-w-[4px]"
            style={{ height: `${(point.value / Math.max(...filteredData.map(d => d.value))) * 100}%` }}
            title={`${point.date}: ${point.value}`}
          />
        ))}
      </div>
    </div>
  );
}

Pasar datos del servidor a componentes cliente

Los datos que pases de Server a Client Components deben ser serializables: strings, números, booleanos, arrays, objetos planos y null. No puedes pasar funciones, Dates, Maps, Sets ni instancias de clase.

typescript
// CORRECTO: Serializar datos antes de pasarlos
// app/[locale]/users/page.tsx (Server Component)
export default async function UsersPage() {
  const users = await db.user.findMany({
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true,
    },
  });

  // Serializar las fechas a strings ISO antes de pasar a client
  const serializedUsers = users.map((user) => ({
    ...user,
    createdAt: user.createdAt.toISOString(),
  }));

  return <UserTable users={serializedUsers} />;
}

// INCORRECTO: Pasar datos no serializables
// Esto causaría un error en runtime:
// <UserTable
//   users={users}        // Date objects no son serializables
//   onDelete={deleteUser} // Funciones no son serializables
// />
typescript
// components/UserTable.tsx
"use client";

import { useState } from "react";

interface SerializedUser {
  id: string;
  name: string;
  email: string;
  createdAt: string; // ISO string, no Date
}

export function UserTable({ users }: { users: SerializedUser[] }) {
  const [sortBy, setSortBy] = useState<"name" | "createdAt">("name");

  const sorted = [...users].sort((a, b) =>
    sortBy === "name"
      ? a.name.localeCompare(b.name)
      : new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
  );

  return (
    <table className="w-full border-collapse">
      <thead>
        <tr className="border-b">
          <th
            className="p-3 text-left cursor-pointer hover:text-primary-600"
            onClick={() => setSortBy("name")}
          >
            Nombre {sortBy === "name" && "↓"}
          </th>
          <th className="p-3 text-left">Email</th>
          <th
            className="p-3 text-left cursor-pointer hover:text-primary-600"
            onClick={() => setSortBy("createdAt")}
          >
            Fecha {sortBy === "createdAt" && "↓"}
          </th>
        </tr>
      </thead>
      <tbody>
        {sorted.map((user) => (
          <tr key={user.id} className="border-b hover:bg-gray-50">
            <td className="p-3">{user.name}</td>
            <td className="p-3 text-gray-600">{user.email}</td>
            <td className="p-3 text-gray-500">
              {new Date(user.createdAt).toLocaleDateString("es-ES")}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Streaming con Suspense y loading.tsx

El streaming permite enviar partes de la página al navegador a medida que están listas, en lugar de esperar a que todo el contenido se genere en el servidor. Esto mejora significativamente la métrica Time to First Byte (TTFB) y la experiencia percibida del usuario.

Archivo loading.tsx

Next.js usa automáticamente un archivo loading.tsx como fallback de Suspense para la ruta donde se coloca:

typescript
// app/[locale]/dashboard/loading.tsx
export default function DashboardLoading() {
  return (
    <div className="container mx-auto px-4 py-8 animate-pulse">
      <div className="h-10 w-48 bg-gray-200 rounded mb-8" />

      {/* Skeleton para cards de estadísticas */}
      <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
        {Array.from({ length: 4 }).map((_, i) => (
          <div key={i} className="h-24 bg-gray-200 rounded-xl" />
        ))}
      </div>

      {/* Skeleton para el gráfico */}
      <div className="h-80 bg-gray-200 rounded-xl mb-8" />

      {/* Skeleton para la lista */}
      <div className="space-y-3">
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="h-16 bg-gray-200 rounded-lg" />
        ))}
      </div>
    </div>
  );
}

Suspense granular

Para un control más fino, envuelve componentes individuales con Suspense para que cada sección se cargue de forma independiente:

typescript
// app/[locale]/dashboard/page.tsx
import { Suspense } from "react";
import { StatsCards, StatsCardsSkeleton } from "@/components/dashboard/StatsCards";
import { RecentActivity, ActivitySkeleton } from "@/components/dashboard/RecentActivity";
import { DashboardChart, ChartSkeleton } from "@/components/dashboard/DashboardChart";

export default function DashboardPage() {
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Dashboard</h1>

      {/* Cada sección se carga independientemente */}
      <Suspense fallback={<StatsCardsSkeleton />}>
        <StatsCards />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <DashboardChart />
      </Suspense>

      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </main>
  );
}

// Cada componente hijo hace su propio fetch
// components/dashboard/StatsCards.tsx (Server Component)
async function StatsCards() {
  const stats = await db.analytics.getStats(); // Puede tardar 200ms

  return (
    <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
      {stats.map((stat) => (
        <div key={stat.label} className="rounded-xl border p-4">
          <p className="text-sm text-gray-500">{stat.label}</p>
          <p className="text-3xl font-bold">{stat.value}</p>
        </div>
      ))}
    </div>
  );
}

Error boundaries con error.tsx

Next.js proporciona un mecanismo integrado de manejo de errores a nivel de ruta mediante el archivo error.tsx. Este actúa como un Error Boundary de React que captura errores tanto en Server como en Client Components dentro de su segmento de ruta.

typescript
// app/[locale]/dashboard/error.tsx
"use client"; // Los error boundaries DEBEN ser Client Components

import { useEffect } from "react";

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Registrar el error en un servicio de monitoreo
    console.error("Error en Dashboard:", error);
  }, [error]);

  return (
    <div className="flex flex-col items-center justify-center py-20">
      <div className="rounded-xl border border-red-200 bg-red-50 p-8 text-center max-w-md">
        <h2 className="text-xl font-semibold text-red-800">
          Algo salió mal
        </h2>
        <p className="mt-2 text-red-600">
          No se pudieron cargar los datos del dashboard.
        </p>
        {error.digest && (
          <p className="mt-1 text-sm text-red-400">
            Código de error: {error.digest}
          </p>
        )}
        <button
          onClick={reset}
          className="mt-4 rounded-lg bg-red-600 px-4 py-2 text-white hover:bg-red-700 transition-colors"
        >
          Intentar de nuevo
        </button>
      </div>
    </div>
  );
}

Puntos clave sobre error.tsx:

  • Siempre es un Client Component: Debe tener "use client" porque Error Boundaries son una funcionalidad del lado del cliente en React.
  • Recibe reset: Una función que permite al usuario reintentar el renderizado del segmento de ruta.
  • Propiedad digest: Un hash del error generado automáticamente por Next.js que se puede usar para rastrear el error en logs del servidor sin exponer detalles sensibles.
  • Alcance de segmento: Cada carpeta puede tener su propio error.tsx, permitiendo UI de error granular por sección.

Manejo de not-found

typescript
// app/[locale]/blog/[slug]/page.tsx
import { notFound } from "next/navigation";

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.post.findUnique({ where: { slug } });

  if (!post) notFound(); // Renderiza el not-found.tsx más cercano

  return <article>{/* ... */}</article>;
}

// app/[locale]/blog/not-found.tsx
export default function BlogNotFound() {
  return (
    <div className="flex flex-col items-center justify-center py-20">
      <h2 className="text-2xl font-bold">Artículo no encontrado</h2>
      <p className="mt-2 text-gray-600">
        El artículo que buscas no existe o ha sido eliminado.
      </p>
    </div>
  );
}

Importaciones dinámicas con next/dynamic

next/dynamic permite cargar componentes de forma diferida (lazy loading), dividiéndolos en chunks separados que solo se descargan cuando se necesitan. Es especialmente útil para componentes pesados o que dependen de APIs del navegador.

typescript
// app/[locale]/page.tsx
import dynamic from "next/dynamic";

// El componente solo se carga cuando se renderiza
const HeavyEditor = dynamic(
  () => import("@/components/editor/RichTextEditor"),
  {
    loading: () => (
      <div className="h-64 animate-pulse rounded-xl bg-gray-200" />
    ),
    ssr: false, // No renderizar en el servidor (usa APIs del navegador)
  }
);

// Componente que solo funciona en el navegador
const MapComponent = dynamic(
  () => import("@/components/maps/InteractiveMap"),
  {
    loading: () => (
      <div className="h-96 rounded-xl bg-gray-100 flex items-center justify-center">
        <p className="text-gray-500">Cargando mapa...</p>
      </div>
    ),
    ssr: false,
  }
);

export default function HomePage() {
  return (
    <main>
      <section className="py-12">
        <h2 className="text-2xl font-bold mb-4">Editor</h2>
        <HeavyEditor />
      </section>

      <section className="py-12">
        <h2 className="text-2xl font-bold mb-4">Ubicación</h2>
        <MapComponent />
      </section>
    </main>
  );
}

Cuándo usar dynamic() vs cuándo usar Suspense:

  • dynamic() con ssr: false: Para componentes que dependen de APIs del navegador (window, document, canvas). Evita errores de hidratación.
  • dynamic() con ssr: true (por defecto): Para code-splitting de componentes pesados. Se renderizan en el servidor pero el JS se carga como chunk separado.
  • Suspense: Para streaming de Server Components async. Permite mostrar un fallback mientras el componente se resuelve en el servidor.

Errores comunes y cómo evitarlos

Error 1: Usar hooks en Server Components

typescript
// INCORRECTO: useState no funciona en Server Components
// app/[locale]/page.tsx
import { useState } from "react"; // Error en runtime

export default function Page() {
  const [count, setCount] = useState(0); // Error: hooks no disponibles
  return <p>{count}</p>;
}

// CORRECTO: Extraer la parte interactiva a un Client Component
// app/[locale]/page.tsx (Server Component)
import { Counter } from "@/components/Counter";

export default function Page() {
  return (
    <main>
      <h1>Mi página</h1>
      <Counter /> {/* Client Component con useState */}
    </main>
  );
}

Error 2: Importar código del servidor en Client Components

typescript
// INCORRECTO: Importar lógica del servidor en un Client Component
"use client";

import { db } from "@/lib/database"; // ERROR: db usa Node.js APIs

export function UserList() {
  // Esto nunca funcionará en el cliente
  const users = db.user.findMany(); // Error en runtime
  return <ul>{/* ... */}</ul>;
}

// CORRECTO: Fetch desde una API route o pasar datos via props
"use client";

import { useState, useEffect } from "react";

export function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("/api/users")
      .then((res) => res.json())
      .then((data) => setUsers(data));
  }, []);

  return <ul>{/* ... */}</ul>;
}

Error 3: Poner "use client" demasiado arriba

typescript
// INCORRECTO: Marcar todo el layout como Client Component
// app/[locale]/layout.tsx
"use client"; // Ahora NADA en esta rama puede ser Server Component

export default function Layout({ children }) {
  return <div>{children}</div>;
}

// CORRECTO: Mantener el layout como Server Component
// Extraer solo las partes interactivas a Client Components
// app/[locale]/layout.tsx (Server Component)
import { Navbar } from "@/components/common/Navbar"; // Client Component
import { Footer } from "@/components/common/Footer"; // Server Component

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <Navbar /> {/* Solo esto es Client Component */}
      {children}  {/* Puede contener Server Components */}
      <Footer /> {/* Server Component: sin interactividad */}
    </div>
  );
}

Error 4: Pasar datos no serializables a Client Components

typescript
// INCORRECTO: Pasar funciones o Dates a Client Components
export default async function Page() {
  const data = await getData();

  return (
    <ClientComponent
      date={new Date()}        // Error: Date no es serializable
      onClick={() => {}}       // Error: funciones no son serializables
      map={new Map()}          // Error: Map no es serializable
    />
  );
}

// CORRECTO: Serializar todo antes de pasarlo
export default async function Page() {
  const data = await getData();

  return (
    <ClientComponent
      date={new Date().toISOString()} // String ISO
      items={Array.from(someMap)}     // Array de tuplas
    />
  );
}

Consejo final: Los React Server Components no son un reemplazo de los Client Components, sino un complemento. La clave está en encontrar el equilibrio correcto: usa Server Components para fetching, transformación de datos y renderizado estático; usa Client Components exclusivamente para interactividad. Mantén la directiva "use client" lo más abajo posible en el árbol de componentes para maximizar los beneficios de RSC. Esta arquitectura produce aplicaciones más rápidas, más ligeras y más fáciles de mantener.

Compartir:

Artículos relacionados