Desarrolla con React y Next.js: Guía Práctica para Latinoamérica

Aprende a crear aplicaciones web escalables con React y Next.js. Descubre cómo mejorar la experiencia del usuario con componentes React y Nextjs app router para desarrolladores de Latinoamérica.

React y Next.js son el stack frontend más demandado en Latinoamérica hoy. Dominarlos abre puertas a proyectos remotos con empresas de toda la región y el mundo. En este artículo voy a mostrarte cómo construir una aplicación web moderna y escalable, con las prácticas que uso en proyectos reales.

¿Por qué React + Next.js?

React por sí solo es una biblioteca UI: maneja el render de componentes y el estado local. Next.js agrega encima todo lo que React no tiene por defecto:

  • Server-Side Rendering (SSR) para SEO y performance
  • Static Site Generation (SSG) para páginas que no cambian
  • File-based routing (sin configurar react-router manualmente)
  • Image Optimization integrada
  • API Routes para endpoints simples
  • Optimizaciones de performance automáticas

Juntos forman uno de los stacks más completos y productivos para aplicaciones web en 2025.

Crear el proyecto

# Crear con create-next-app (configuración guiada)
npx create-next-app@latest mi-app --typescript --tailwind --eslint --app

cd mi-app
npm run dev

El flag --app usa el App Router (el nuevo sistema de routing de Next.js 13+). Si preferís el routing anterior más simple, omitilo (usa Pages Router).

Componentes React en el App Router

Con el App Router, hay dos tipos de componentes que necesitás entender bien:

Server Components (por defecto)

// app/productos/page.tsx
// Este componente corre EN EL SERVIDOR. No puede usar hooks ni eventos.
async function ProductosPage() {
  // Fetch directo desde el servidor, sin useEffect
  const productos = await fetch('https://api.mitienda.com/productos')
    .then(r => r.json());

  return (
    <main>
      <h1>Productos</h1>
      <div className="grid grid-cols-3 gap-4">
        {productos.map(producto => (
          <TarjetaProducto key={producto.id} producto={producto} />
        ))}
      </div>
    </main>
  );
}

export default ProductosPage;

Ventaja: cero JavaScript enviado al cliente para este componente. El HTML llega renderizado.

Client Components

// components/BuscadorProductos.tsx
'use client'; // ← Marca este componente como cliente

import { useState } from 'react';

export function BuscadorProductos({ onBuscar }: { onBuscar: (q: string) => void }) {
  const [query, setQuery] = useState('');

  return (
    <input
      type="text"
      value={query}
      onChange={e => setQuery(e.target.value)}
      onKeyDown={e => e.key === 'Enter' && onBuscar(query)}
      placeholder="Buscar productos..."
      className="border rounded px-4 py-2 w-full"
    />
  );
}

Regla de oro: usá Client Components solo cuando necesitás interactividad (useState, useEffect, eventos del DOM). Todo lo demás, Server Components.

Routing con el App Router

El routing es basado en carpetas dentro de app/:

app/
├── page.tsx              → /
├── layout.tsx            → Layout que envuelve todo
├── productos/
│   ├── page.tsx          → /productos
│   └── [id]/
│       └── page.tsx      → /productos/123
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/mi-articulo
└── api/
    └── webhook/
        └── route.ts      → /api/webhook (API Route)

Layout compartido

// app/layout.tsx
import type { Metadata } from 'next';
import { Navbar } from '@/components/Navbar';
import { Footer } from '@/components/Footer';
import './globals.css';

export const metadata: Metadata = {
  title: {
    template: '%s | Mi Tienda',
    default: 'Mi Tienda Online',
  },
  description: 'La mejor tienda online de Latinoamérica',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="es">
      <body>
        <Navbar />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Rutas dinámicas con datos

// app/productos/[id]/page.tsx
import { notFound } from 'next/navigation';

interface Props {
  params: { id: string };
}

export async function generateMetadata({ params }: Props) {
  const producto = await obtenerProducto(params.id);
  return {
    title: producto?.nombre,
    description: producto?.descripcion,
  };
}

export default async function ProductoDetallePage({ params }: Props) {
  const producto = await obtenerProducto(params.id);

  if (!producto) {
    notFound(); // Renderiza la página 404
  }

  return (
    <article>
      <h1>{producto.nombre}</h1>
      <p className="text-2xl font-bold">${producto.precio}</p>
      <p>{producto.descripcion}</p>
    </article>
  );
}

// Pre-renderizar las páginas más visitadas en build time (SSG)
export async function generateStaticParams() {
  const productos = await obtenerProductosDestacados();
  return productos.map(p => ({ id: p.id.toString() }));
}

Hooks esenciales de React

useState y useEffect

'use client';
import { useState, useEffect } from 'react';

function ListaProductos() {
  const [productos, setProductos] = useState<Producto[]>([]);
  const [cargando, setCargando] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch('/api/productos')
      .then(r => r.json())
      .then(data => setProductos(data))
      .catch(err => setError(err.message))
      .finally(() => setCargando(false));
  }, []); // Array vacío = ejecutar solo al montar

  if (cargando) return <Spinner />;
  if (error) return <MensajeError mensaje={error} />;

  return (
    <ul>
      {productos.map(p => (
        <li key={p.id}>{p.nombre}</li>
      ))}
    </ul>
  );
}

useCallback y useMemo para optimización

'use client';
import { useState, useCallback, useMemo } from 'react';

function CatalogoFiltrable({ productos }: { productos: Producto[] }) {
  const [filtro, setFiltro] = useState('');
  const [ordenar, setOrdenar] = useState<'precio' | 'nombre'>('nombre');

  // useMemo: recalcula solo cuando cambian filtro u ordenar
  const productosFiltrados = useMemo(() => {
    return productos
      .filter(p => p.nombre.toLowerCase().includes(filtro.toLowerCase()))
      .sort((a, b) => a[ordenar] > b[ordenar] ? 1 : -1);
  }, [productos, filtro, ordenar]);

  // useCallback: función estable que no se recrea en cada render
  const handleFiltro = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setFiltro(e.target.value);
  }, []);

  return (
    <>
      <input onChange={handleFiltro} placeholder="Filtrar..." />
      <p>{productosFiltrados.length} productos</p>
      <ul>
        {productosFiltrados.map(p => <li key={p.id}>{p.nombre}</li>)}
      </ul>
    </>
  );
}

Manejo de formularios con Server Actions

En el App Router podés manejar formularios sin JavaScript del lado del cliente:

// app/contacto/page.tsx
import { redirect } from 'next/navigation';

async function enviarContacto(formData: FormData) {
  'use server';
  const nombre = formData.get('nombre') as string;
  const email = formData.get('email') as string;
  const mensaje = formData.get('mensaje') as string;

  await enviarEmail({ nombre, email, mensaje });
  redirect('/contacto/gracias');
}

export default function PaginaContacto() {
  return (
    <form action={enviarContacto}>
      <input name="nombre" placeholder="Tu nombre" required />
      <input name="email" type="email" placeholder="Tu email" required />
      <textarea name="mensaje" placeholder="Tu mensaje" required />
      <button type="submit">Enviar</button>
    </form>
  );
}

Optimización de imágenes

Next.js tiene el componente Image que optimiza automáticamente:

import Image from 'next/image';

function BannerHero() {
  return (
    <Image
      src="/banner.jpg"
      alt="Banner principal de la tienda"
      width={1200}
      height={600}
      priority      // Preload para la imagen above-the-fold
      placeholder="blur"
      className="w-full h-auto"
    />
  );
}

Beneficios automáticos:

  • Conversión a WebP/AVIF
  • Lazy loading por defecto (excepto con priority)
  • Responsive con srcset
  • Prevención de layout shift

Variables de entorno

# .env.local (no va al repo)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
NEXT_PUBLIC_API_URL=https://api.mitienda.com
// Las que empiezan con NEXT_PUBLIC_ son accesibles en el cliente
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// Las que no, solo en el servidor (Server Components, API Routes)
const dbUrl = process.env.DATABASE_URL;

Deployment en Vercel

# Instalar CLI de Vercel
npm i -g vercel

# Deploy desde tu proyecto local
vercel

# Deploy de producción desde la rama main
vercel --prod

O conectar el repo de GitHub directamente en vercel.com — cada push a main dispara un deploy automático.

Conclusión

React y Next.js siguen siendo la combinación más demandada para desarrollo frontend profesional en Latinoamérica. Con el App Router, Next.js logra un balance excelente entre DX (Developer Experience) y performance.

Mi recomendación para aprender: empezá con el Pages Router si sos nuevo en Next.js, luego migrá al App Router una vez que tengas los conceptos básicos de React claros (componentes, hooks, estado). El salto es más fácil si ya entendés bien los fundamentos.

¿Querés ver React y Next.js en proyectos reales? Podés revisar mi portfolio o escribirme por contacto.

Mauricio González — Full Stack Developer

Mauricio González Full Stack Developer

5+ años desarrollando aplicaciones web y móviles con React, NestJS, TypeScript y Flutter. Basado en Paraguay, disponible para trabajo remoto en Latinoamérica.