Saltar al contenido principal

Componentes, Estilos, Estado Global y API Routes

9. Componentes y Renderizado

Regla

9.1: Se deben utilizar Componentes React estándar para construir la interfaz de usuario. 9.2: Se debe favorecer el uso de Server Components por defecto para mejorar el rendimiento, recurriendo a Client Components ('use client') solo cuando sea estrictamente necesario (interactividad, hooks de estado/efecto, APIs del navegador). 9.3: El código dentro de Server Components no debe depender de APIs exclusivas del navegador (ej. window, localStorage) ni de estado o efectos de React (useState, useEffect). 9.4: Se debe estructurar el código para aislar la lógica de cliente en Client Components específicos, importándolos en Server Components cuando sea necesario. 9.5: Se debe considerar el uso de Suspense para gestionar la carga de componentes asíncronos y mejorar la experiencia de usuario.

Contexto/Justificación

Next.js App Router introduce Server y Client Components para optimizar el rendimiento y la carga de JavaScript. Los Server Components se renderizan en el servidor y no envían JS al cliente, mientras que los Client Components permiten interactividad. Separar la lógica adecuadamente es crucial para aprovechar estas optimizaciones. Suspense permite el streaming y la carga selectiva.

Ejemplos

Correcto (Server Component importando Client Component):

// app/page.jsx (Server Component)
import ClientButton from './ClientButton';

export default function Page() {
return (
<div>
<h1>Bienvenido (Server Component)</h1>
<ClientButton /> {/* Componente de cliente para interactividad */}
</div>
);
}

// app/ClientButton.jsx
'use client'; // Directiva necesaria

import { useState } from 'react';

export default function ClientButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clics: {count}</button>;
}

Incorrecto (Uso de hook de cliente en Server Component):

// app/page.jsx (Error - Server Component)
import { useState } from 'react'; // ¡Error! useState no funciona aquí

export default function Page() {
const [count, setCount] = useState(0); // ¡Error!
return <div>Contador: {count}</div>;
}

Cuándo aplicar

  • Siempre que se creen componentes en una aplicación Next.js con App Router.

Cuándo evitar

  • En proyectos Next.js que aún utilicen el Pages Router (aunque la migración es recomendable).

9.1. Regla sobre Optimización de Client Components y Suspense

Regla Principal

Regla: La IA debe minimizar y optimizar el uso de Client Components creando wrappers específicos y utilizando Suspense para mejorar la experiencia de usuario.

  1. Wrappers Pequeños: Crear Client Components pequeños y específicos que envuelvan únicamente los elementos que requieren interactividad, manteniendo el resto como Server Components.
  2. Suspense Obligatorio: Envolver Client Components en Suspense con fallback UI apropiado para mostrar estados de carga y mejorar la percepción de rendimiento.
  3. Aislamiento de Interactividad: Aislar la lógica interactiva (state, effects, event handlers) en Client Components específicos, importándolos en Server Components cuando sea necesario.
  4. Minimización de Bundling: Reducir el JavaScript enviado al cliente manteniendo la mayor cantidad posible de lógica en Server Components.

Contexto y Justificación

Los Client Components añaden JavaScript al bundle del cliente, impactando el rendimiento. Crear wrappers pequeños y específicos permite mantener la mayoría del código como Server Components, reduciendo el bundle size. Suspense mejora la experiencia de usuario al mostrar estados de carga instantáneos mientras se cargan los componentes interactivos, especialmente importante en conexiones lentas.

Ejemplos y Contraejemplos

  • Correcto:
        // components/ui/InteractiveButton.tsx (Client Component pequeño)
    'use client';
    import { useState } from 'react';

    interface InteractiveButtonProps {
    children: React.ReactNode;
    initialCount?: number;
    }

    export function InteractiveButton({ children, initialCount = 0 }: InteractiveButtonProps) {
    const [count, setCount] = useState(initialCount);

    return (
    <button
    onClick={() => setCount(count + 1)}
    className="px-4 py-2 bg-blue-500 text-white rounded"
    >
    {children} ({count})
    </button>
    );
    }

    // app/page.tsx (Server Component que usa el client component)
    import { Suspense } from 'react';
    import { InteractiveButton } from '@/components/ui/InteractiveButton';

    export default function HomePage() {
    return (
    <div className="container mx-auto p-4">
    <h1>Página Principal</h1>
    <p>Este contenido se renderiza en el servidor</p>

    <Suspense fallback={<div className="animate-pulse">Cargando botón...</div>}>
    <InteractiveButton>
    Haz clic aquí
    </InteractiveButton>
    </Suspense>

    <footer>
    <p>Footer también renderizado en servidor</p>
    </footer>
    </div>
    );
    }
  • Incorrecto:
       // ❌ Marcar todo el componente como client component innecesariamente
    'use client';
    import { useState } from 'react';

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

    return (
    <div className="container mx-auto p-4">
    <h1>Página Principal</h1>
    <p>Este contenido NO necesita ser client component</p>

    {/* Solo este botón necesita interactividad */}
    <button onClick={() => setCount(count + 1)}>
    Haz clic aquí ({count})
    </button>

    <footer>
    <p>Footer tampoco necesita ser client component</p>
    </footer>
    </div>
    );
    }

    // ❌ No usar Suspense con Client Components
    import { InteractiveButton } from '@/components/ui/InteractiveButton';

    export default function HomePage() {
    return (
    <div>
    <InteractiveButton>Sin Suspense</InteractiveButton>
    </div>
    );
    }

Cuándo Aplicar

Siempre que se necesite añadir interactividad a una aplicación Next.js, creando la mínima superficie de Client Components posible.

Cuándo Evitar o Flexibilizar

En casos donde toda la página requiere interactividad compleja (ej. dashboards con múltiples estados interdependientes), puede ser apropiado marcar componentes más grandes como Client Components, pero siempre evaluando si se pueden dividir en partes más pequeñas.

10. Estilos y CSS

Regla

10.1: Se debe utilizar CSS Modules por defecto para estilos específicos de componentes, garantizando el encapsulamiento y evitando colisiones de nombres. Los archivos deben seguir la nomenclatura [nombre].module.css. 10.2: Para estilos globales, se debe usar un archivo CSS global (ej. app/globals.css) importado únicamente en el layout raíz (app/layout.tsx). 10.3: Se permite el uso de Tailwind CSS como alternativa o complemento a CSS Modules, configurándolo adecuadamente en el proyecto. Su uso debe ser consistente. 10.4: Se permite el uso de bibliotecas CSS-in-JS compatibles con Server Components y streaming (ej. Styled Components with specific configuration, Emotion with specific configuration) if the project requires it, but CSS Modules or Tailwind must be the first option. 10.5: No se deben usar selectores globales o de etiqueta directamente en CSS Modules para evitar efectos secundarios inesperados; usar clases.

Contexto/Justificación

Next.js ofrece varias formas de manejar estilos. CSS Modules proporciona encapsulamiento local por defecto. Los estilos globales son necesarios para configuraciones base. Tailwind CSS es popular por su enfoque utility-first. CSS-in-JS requiere configuración adicional en el App Router para funcionar correctamente con Server Components. La consistencia es clave.

Ejemplos

Correcto (CSS Modules):

// components/Button/Button.module.css
.button {
background-color: blue;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
}

// components/Button/Button.jsx
import styles from './Button.module.css';

export default function Button({ children }) {
return <button className={styles.button}>{children}</button>;
}

Correcto (Tailwind CSS):

// components/Card/Card.jsx
export default function Card({ title, children }) {
return (
<div className="p-4 border rounded shadow-md bg-white">
<h2 className="text-xl font-bold mb-2">{title}</h2>
<div>{children}</div>
</div>
);
}

Correcto (Importación global):

// app/layout.tsx
import './globals.css'; // Importación única de estilos globales

export default function RootLayout({ children }) {
return (
<html lang="es">
<body>{children}</body>
</html>
);
}

Cuándo aplicar

  • Al definir estilos para componentes o la aplicación global en Next.js.

Cuándo evitar

  • Usar estilos inline (style={{}}) para más que ajustes dinámicos o muy simples.
  • Importar múltiples archivos CSS globales en diferentes lugares.

11. Manejo del Estado Global

Regla

11.1: Para estado global complejo o compartido entre múltiples componentes no relacionados directamente, se debe utilizar una biblioteca de manejo de estado (ej. Zustand, Redux Toolkit, Jotai) o React Context API. 11.2: Al usar React Context API, se debe envolver el proveedor (Provider) lo más cerca posible de los componentes que consumen el estado para optimizar re-renderizados. 11.3: Los proveedores de contexto que necesiten estado (useState) deben marcarse con 'use client' y no deben colocarse directamente en el layout raíz si este es un Server Component. Se debe crear un componente cliente intermedio para el proveedor. 11.4: Para bibliotecas como Zustand o Redux Toolkit, se debe seguir la documentación oficial para la integración con Next.js App Router, especialmente respecto a la inicialización y el uso en Server/Client Components. 11.5: No se debe abusar del estado global; si un estado solo es relevante para un componente y sus descendientes directos, se debe usar estado local (useState) o "prop drilling" controlado.

Contexto/Justificación

El manejo de estado global es esencial en aplicaciones complejas. React Context es una solución integrada, pero puede causar re-renderizados innecesarios si no se usa correctamente. Bibliotecas externas como Zustand o Redux ofrecen soluciones más optimizadas y escalables. La integración con el App Router (Server/Client Components) requiere atención específica para evitar errores y asegurar la hidratación correcta.

Ejemplos

Correcto (React Context con Componente Cliente Proveedor):

// context/AuthContext.js
'use client';
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// Lógica para login, logout, etc.
const value = { user, setUser };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
return useContext(AuthContext);
}

// app/providers.jsx
'use client'; // Este componente envuelve a los proveedores de cliente
import { AuthProvider } from '@/context/AuthContext';

export function Providers({ children }) {
return <AuthProvider>{children}</AuthProvider>;
}

// app/layout.jsx (Server Component)
import { Providers } from './providers'; // Importa el wrapper de cliente

export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers> {/* Usa el wrapper aquí */}
</body>
</html>
);
}

// app/SomeClientComponent.jsx
'use client';
import { useAuth } from '@/context/AuthContext';

export default function SomeClientComponent() {
const { user } = useAuth();
return <div>{user ? `Hola, ${user.name}` : 'Invitado'}</div>;
}

Correcto (Zustand - Simplificado):

// store/authStore.js
import { create } from 'zustand';

const useAuthStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));

export default useAuthStore;

// app/SomeClientComponent.jsx
'use client';
import useAuthStore from '@/store/authStore';

export default function SomeClientComponent() {
const user = useAuthStore((state) => state.user);
// ... usar estado y acciones
}

Cuándo aplicar

  • Cuando se necesita compartir estado entre componentes que no tienen una relación padre-hijo directa.
  • Para gestionar datos de sesión de usuario, preferencias, estado de UI complejo (ej. modal abierto).

Cuándo evitar

  • Para estado que lógicamente pertenece a un solo componente o a un subárbol pequeño (usar useState).
  • Cuando la obtención de datos del servidor (fetch, Server Actions) puede reemplazar la necesidad de almacenar esos datos globalmente en el cliente.

11.1. Regla sobre Gestión de Estado con URL Search Params

Regla Principal

Regla: Para estado que necesita ser compartible, persistente entre recargas de página o navegable mediante URL, la IA debe utilizar URL search parameters como mecanismo principal de almacenamiento.

  1. URL Search Params: Utilizar URL search parameters para estado que debe ser compartible (ej. filtros, paginación, configuraciones de vista).
  2. Librería nuqs: Implementar nuqs como librería preferida para simplificar el manejo de URL search params con React hooks.
  3. Client Components: Marcar componentes que manejen URL state con 'use client' ya que requieren acceso a la navegación del navegador.
  4. Fallback Values: Proporcionar valores por defecto apropiados para cuando los parámetros no están presentes en la URL.
  5. Tipos de Datos: Manejar correctamente la serialización/deserialización de diferentes tipos de datos (strings, numbers, booleans, arrays).

Contexto y Justificación

La gestión de estado mediante URL search params permite crear interfaces que son compartibles mediante enlaces y mantienen el estado al recargar la página. Esto mejora significativamente la experiencia de usuario en aplicaciones con filtros, búsquedas o configuraciones complejas. La librería nuqs simplifica el manejo de estos parámetros proporcionando hooks que se sincronizan automáticamente con la URL.

Ejemplos y Contraejemplos

  • Correcto:
    // components/ProductFilter.tsx
    'use client';
    import { useQueryState, parseAsString, parseAsInteger } from 'nuqs';

    export function ProductFilter() {
    const [category, setCategory] = useQueryState('category', parseAsString.withDefault('all'));
    const [minPrice, setMinPrice] = useQueryState('min_price', parseAsInteger.withDefault(0));
    const [maxPrice, setMaxPrice] = useQueryState('max_price', parseAsInteger.withDefault(1000));

    return (
    <div className="space-y-4">
    <select
    value={category}
    onChange={(e) => setCategory(e.target.value)}
    >
    <option value="all">Todas las categorías</option>
    <option value="electronics">Electrónicos</option>
    <option value="clothing">Ropa</option>
    </select>

    <div className="flex gap-2">
    <input
    type="number"
    placeholder="Precio mínimo"
    value={minPrice}
    onChange={(e) => setMinPrice(parseInt(e.target.value) || 0)}
    />
    <input
    type="number"
    placeholder="Precio máximo"
    value={maxPrice}
    onChange={(e) => setMaxPrice(parseInt(e.target.value) || 1000)}
    />
    </div>
    </div>
    );
    }

    // components/SearchComponent.tsx
    'use client';
    import { useQueryState, parseAsString, parseAsArrayOf } from 'nuqs';

    export function SearchComponent() {
    const [query, setQuery] = useQueryState('q', parseAsString.withDefault(''));
    const [tags, setTags] = useQueryState('tags', parseAsArrayOf(parseAsString).withDefault([]));

    const addTag = (tag: string) => {
    if (!tags.includes(tag)) {
    setTags([...tags, tag]);
    }
    };

    const removeTag = (tagToRemove: string) => {
    setTags(tags.filter(tag => tag !== tagToRemove));
    };

    return (
    <div>
    <input
    type="text"
    placeholder="Buscar..."
    value={query}
    onChange={(e) => setQuery(e.target.value)}
    />
    <div className="flex gap-2 mt-2">
    {tags.map(tag => (
    <span key={tag} className="bg-blue-100 px-2 py-1 rounded">
    {tag}
    <button onClick={() => removeTag(tag)}>×</button>
    </span>
    ))}
    </div>
    </div>
    );
    }
  • Incorrecto:
    // ❌ Usar useState para estado que debería ser compartible
    'use client';
    import { useState } from 'react';

    export function ProductFilter() {
    const [category, setCategory] = useState('all'); // No es compartible
    const [filters, setFilters] = useState({ minPrice: 0, maxPrice: 1000 }); // Se pierde al recargar

    // Este estado se pierde al navegar o recargar
    return (
    <select value={category} onChange={(e) => setCategory(e.target.value)}>
    <option value="all">Todas las categorías</option>
    </select>
    );
    }

    // ❌ Manipular URL manualmente sin librería
    'use client';
    import { useRouter, useSearchParams } from 'next/navigation';

    export function SearchComponent() {
    const router = useRouter();
    const searchParams = useSearchParams();

    const updateQuery = (newQuery: string) => {
    const params = new URLSearchParams(searchParams.toString());
    params.set('q', newQuery);
    router.push(`?${params.toString()}`); // Código verboso y propenso a errores
    };

    return <input onChange={(e) => updateQuery(e.target.value)} />;
    }

Cuándo Aplicar

Para estado que necesita ser compartible mediante URLs, persistente entre recargas de página o que forma parte de la navegación del usuario (filtros, búsquedas, paginación, configuraciones de vista).

Cuándo Evitar o Flexibilizar

Para estado temporal o interno de componentes (ej. estado de modales, animaciones, formularios en progreso), estado sensible que no debe ser visible en la URL, o estado que cambia muy frecuentemente y causaría navegación excesiva.

12. API Routes (App Router)

Regla

12.1: Para crear endpoints de API dentro de la aplicación Next.js, se deben utilizar Route Handlers en el App Router. Estos se definen en archivos route.js o route.ts dentro de las carpetas de ruta (app/api/...). 12.2: Cada archivo route.ts debe exportar funciones asíncronas nombradas según los métodos HTTP que manejan (ej. GET, POST, PUT, DELETE). 12.3: Se debe utilizar el objeto Request para acceder a la información de la petición (headers, body, etc.) y la función NextResponse para construir las respuestas, incluyendo el estado HTTP y el cuerpo (usualmente JSON). 12.4: La lógica de negocio compleja o el acceso a datos deben abstraerse en funciones o servicios separados, manteniendo los Route Handlers como controladores ligeros. 12.5: Se deben implementar mecanismos de validación de entrada (ej. Zod) y manejo de errores adecuado en todos los endpoints. 12.6: No se deben realizar operaciones de larga duración directamente dentro de un Route Handler síncrono que bloqueen la respuesta; usar async/await.

Contexto/Justificación

Los Route Handlers son la forma estándar de crear APIs backend directamente dentro de una aplicación Next.js usando el App Router. Permiten construir endpoints RESTful o de otro tipo que pueden ser consumidos por el frontend de la propia aplicación o por clientes externos. Mantenerlos organizados, seguros y eficientes es crucial.

Ejemplos

Correcto (GET y POST Route Handler):

// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db'; // Módulo simulado de acceso a datos

// Esquema de validación para nuevos usuarios
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});

// Manejador GET para obtener usuarios
export async function GET(request: Request) {
try {
const users = await db.getUsers();
return NextResponse.json(users);
} catch (error) {
console.error("Error fetching users:", error);
return NextResponse.json({ message: 'Error interno del servidor' }, { status: 500 });
}
}

// Manejador POST para crear un usuario
export async function POST(request: Request) {
try {
const body = await request.json();
const validation = userSchema.safeParse(body);

if (!validation.success) {
return NextResponse.json({ errors: validation.error.errors }, { status: 400 });
}

const newUser = await db.createUser(validation.data);
return NextResponse.json(newUser, { status: 201 });

} catch (error) {
console.error("Error creating user:", error);
if (error instanceof SyntaxError) { // Error de parseo JSON
return NextResponse.json({ message: 'Cuerpo JSON inválido' }, { status: 400 });
}
return NextResponse.json({ message: 'Error interno del servidor' }, { status: 500 });
}
}

Cuándo aplicar

  • Para crear endpoints de API que sirvan datos o realicen acciones para el frontend.
  • Para manejar webhooks de servicios externos.
  • Para crear un backend simple directamente integrado con el frontend Next.js.

Cuándo evitar

  • Para lógica que puede ser ejecutada directamente en Server Components o Server Actions si no se necesita un endpoint HTTP expuesto.
  • Para construir APIs muy complejas o microservicios desacoplados (considerar un framework backend dedicado como NestJS).