January 29, 2026
React + Claude Code: Cómo No Liarla Parda con la Seguridad y el Rendimiento
O cómo conseguir que una IA te ayude a programar sin que tu aplicación acabe siendo un coladero de hackers

By Fernando Bernal
7 min read
Vamos al lío
Seamos sinceros: todos hemos copiado código de Stack Overflow a las tantas de la madrugada sin mirar muy bien qué hacía. Ahora imagina eso, pero con una IA que genera código a velocidad de vértigo. Bienvenidos a 2026.
Claude Code es la herramienta de Anthropic que ha venido a revolucionar cómo escribimos código. Pero ojo, que aquí viene el dato que os va a quitar el sueño: según Veracode, el código generado por IA la caga en temas de seguridad el 45% de las veces. Casi la mitad. Vamos, que es como tirar una moneda al aire para decidir si tu app va a ser hackeada.
¿Significa esto que tiramos Claude Code a la basura? Ni de coña. Significa que tenemos que aprender a usarlo bien. Y para eso estoy aquí, para contaros cómo integrar React con Claude Code sin acabar llorando en posición fetal cuando vuestro jefe os pregunte por qué hay datos de usuarios pululando por internet.
¿Por qué Claude Code mola tanto para React?
Antes de meternos en harina, dejadme que os explique por qué esto no es otro hype más del mundillo tech.
Claude Code no es el típico autocompletado con esteroides. Este bicho puede:
- Leer tu proyecto entero: Sí, incluido ese componente de 18.000 líneas que "ibas a refactorizar la semana que viene" desde hace dos años.
- Ejecutar comandos: Puede correr tests, hacer builds, y básicamente hacer de junior mientras tú te tomas un café.
- Trabajar en paralelo: Mientras otros asistentes se quedan colgados intentando resolver un parche, Claude Code sigue a lo suyo como si nada.
Un desarrollador de Builder.io lo resumió perfectamente: "Ningún otro agente de IA ha conseguido actualizar ese archivo de 18.000 líneas excepto Claude Code." Y mira que lo han intentado.
Parte 1: Seguridad (o cómo evitar que tu app sea noticia)
El elefante en la habitación
Vale, os cuento una realidad incómoda. Cuando le pedís a Claude Code que genere un componente, existe la posibilidad de que os cuele alguna de estas joyas:
- XSS: Para que cualquier gracioso pueda inyectar scripts en tu web
- CSRF: El clásico "haz clic aquí para ver gatitos" que vacía cuentas bancarias
- Credenciales expuestas: API keys en el código como quien deja las llaves de casa debajo del felpudo
No es culpa de Claude Code, es que las IAs aprenden de código que existe ahí fuera… y hay MUCHO código malo ahí fuera.
Truco 1: Instala los Skills de Seguridad (en serio, hazlo)
Lo primero que deberíais hacer, incluso antes de escribir un "Hola Mundo", es instalar los skills de seguridad:
npx secure-claude-skills init --sync subtreenpx secure-claude-skills init --sync subtreeEs como ponerle un cinturón de seguridad a Claude Code. ¿Incómodo? Un poco. ¿Te salva la vida? Absolutamente.
Truco 2: XSS — El clásico que nunca muere
El XSS es como las cucarachas: lleva décadas con nosotros y parece imposible de erradicar.
Así es como se la lía:
// DANGER ZONE
function ComentarioUsuario({ comentario }: { comentario: string }) {
return <div dangerouslySetInnerHTML={{ __html: comentario }} />;
}// DANGER ZONE
function ComentarioUsuario({ comentario }: { comentario: string }) {
return <div dangerouslySetInnerHTML={{ __html: comentario }} />;
}Ese dangerouslySetInnerHTML no se llama "dangerous" porque mole. Se llama así porque es PELIGROSO. Es como esa función de JavaScript que empieza por eval — si la usas sin protección, mereces lo que te pase.
Así se hace bien:
// Modo buen profesional
import DOMPurify from 'dompurify';
function ComentarioUsuario({ comentario }: { comentario: string }) {
return (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(comentario)
}}
/>
);
}
// ✅ Modo ninja (la mejor opción)
function ComentarioUsuario({ comentario }: { comentario: string }) {
return <div>{comentario}</div>;
}// Modo buen profesional
import DOMPurify from 'dompurify';
function ComentarioUsuario({ comentario }: { comentario: string }) {
return (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(comentario)
}}
/>
);
}
// ✅ Modo ninja (la mejor opción)
function ComentarioUsuario({ comentario }: { comentario: string }) {
return <div>{comentario}</div>;
}Prompt mágico para Claude Code:
"Genera un componente de comentarios. IMPORTANTE: sanitiza todo el contenido de usuario con DOMPurify. Si puedes evitar dangerouslySetInnerHTML, mejor que mejor."
Truco 3: CSRF — El ataque que tu madre podría ejecutar sin querer
CSRF es ese ataque en el que alguien te manda un enlace aparentemente inocente, y al hacer clic, resulta que has transferido dinero, cambiado tu contraseña, o te has suscrito a la newsletter de tu ex.
React NO protege contra esto de serie. Cero. Nada. Así que cuando crees formularios, asegúrate de pedir protección:
'use client';
import { useState } from 'react';
export function FormularioSeguro() {
const [datos, setDatos] = useState({
nombre: '',
email: '',
mensaje: ''
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Paso 1: Pillamos el token CSRF
const csrfRes = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await csrfRes.json();
// Paso 2: Enviamos con el token
const response = await fetch('/api/contacto', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // El secreto está aquí
},
credentials: 'include',
body: JSON.stringify(datos)
});
// Y aquí seguiríamos con la lógica...
}
return (
<form onSubmit={handleSubmit}>
{/* Los campos del formulario */}
</form>
);
}'use client';
import { useState } from 'react';
export function FormularioSeguro() {
const [datos, setDatos] = useState({
nombre: '',
email: '',
mensaje: ''
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Paso 1: Pillamos el token CSRF
const csrfRes = await fetch('/api/csrf', {
credentials: 'include'
});
const { csrfToken } = await csrfRes.json();
// Paso 2: Enviamos con el token
const response = await fetch('/api/contacto', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // El secreto está aquí
},
credentials: 'include',
body: JSON.stringify(datos)
});
// Y aquí seguiríamos con la lógica...
}
return (
<form onSubmit={handleSubmit}>
{/* Los campos del formulario */}
</form>
);
}Truco 4: Credenciales — No seas ESOS developers
Todos hemos visto (o sido) ese developer que commitea API keys al repo. Hay bots que se pasan el día escaneando GitHub buscando exactamente eso. Es como dejar billetes de 50€ en la calle.
El hall of shame:
// Mal. Muy mal. Fatal.
const API_KEY = "pk_live_abc123xyz";
// También mal (XSS puede leer localStorage)
localStorage.setItem('token', response.token);// Mal. Muy mal. Fatal.
const API_KEY = "pk_live_abc123xyz";
// También mal (XSS puede leer localStorage)
localStorage.setItem('token', response.token);Cómo se hace:
// Variables de entorno (como los mayores)
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
// HttpOnly cookies (gestionadas desde el servidor)
// Aquí el frontend ni toca el token// Variables de entorno (como los mayores)
const API_KEY = process.env.NEXT_PUBLIC_API_KEY;
// HttpOnly cookies (gestionadas desde el servidor)
// Aquí el frontend ni toca el tokenTruco 5: Crea tu propio auditor de seguridad
Mola mucho tener un "colega" que te revise el código. Crea este archivo en .claude/agents/security.md:
# Auditor de Seguridad
Eres un experto en seguridad. Tu trabajo es buscar vulnerabilidades.
## Checklist
- [ ] ¿Se validan todos los inputs?
- [ ] ¿Hay credenciales hardcodeadas?
- [ ] ¿Los datos sensibles están cifrados?
- [ ] ¿Hay protección contra XSS?
- [ ] ¿Hay protección contra CSRF?
- [ ] ¿Las dependencias están actualizadas?
Sé estricto. Es mejor un false positive que una brecha de seguridad.# Auditor de Seguridad
Eres un experto en seguridad. Tu trabajo es buscar vulnerabilidades.
## Checklist
- [ ] ¿Se validan todos los inputs?
- [ ] ¿Hay credenciales hardcodeadas?
- [ ] ¿Los datos sensibles están cifrados?
- [ ] ¿Hay protección contra XSS?
- [ ] ¿Hay protección contra CSRF?
- [ ] ¿Las dependencias están actualizadas?
Sé estricto. Es mejor un false positive que una brecha de seguridad.Parte 2: Rendimiento (porque a nadie le gusta esperar)
Vale, tu app es segura. Genial. Pero si tarda 15 segundos en cargar, tus usuarios se irán a la competencia más rápido de lo que puedes decir "optimización prematura".
Vercel ha sacado recientemente una guía con más de 40 reglas de optimización. Os resumo las que de verdad importan.
Prioridad CRÍTICA: Los malditos waterfalls
Os cuento un secreto: la mayoría de los problemas de rendimiento NO son por re-renders innecesarios. Son por hacer peticiones en cascada como un imbécil.
El código del becario:
// ❌ Cada petición espera a la anterior. Zzzzz...
async function cargarDatos() {
const usuario = await fetchUsuario(); // 200ms
const posts = await fetchPosts(usuario.id); // 300ms (espera al anterior)
const comentarios = await fetchComentarios(); // 250ms (espera a los dos)
// Total: 750ms mínimo
return { usuario, posts, comentarios };
}// ❌ Cada petición espera a la anterior. Zzzzz...
async function cargarDatos() {
const usuario = await fetchUsuario(); // 200ms
const posts = await fetchPosts(usuario.id); // 300ms (espera al anterior)
const comentarios = await fetchComentarios(); // 250ms (espera a los dos)
// Total: 750ms mínimo
return { usuario, posts, comentarios };
}El código del senior:
// ✅ Todo a la vez, bro
async function cargarDatos() {
const [usuario, posts, comentarios] = await Promise.all([
fetchUsuario(), // 200ms ─┐
fetchPosts(), // 300ms ─┼─ En paralelo
fetchComentarios() // 250ms ─┘
]);
// Total: 300ms (el más lento de los tres)
return { usuario, posts, comentarios };
}// ✅ Todo a la vez, bro
async function cargarDatos() {
const [usuario, posts, comentarios] = await Promise.all([
fetchUsuario(), // 200ms ─┐
fetchPosts(), // 300ms ─┼─ En paralelo
fetchComentarios() // 250ms ─┘
]);
// Total: 300ms (el más lento de los tres)
return { usuario, posts, comentarios };
}Hemos pasado de 750ms a 300ms con un cambio de 2 líneas. De nada.
Prioridad CRÍTICA: El bundle size (o por qué tu app pesa como un elefante)
Os cuento un caso real: un proyecto pasó de 2.33MB a 236KB. Sí, una reducción del 90%. ¿El truco? Dejar de importar librerías enteras como quien descarga una discografía completa para escuchar una canción.
El clásico error:
// Importa 2.000+ iconos aunque solo uses 5
import * as Icons from 'simple-icons';
const icono = Icons.siReact;// Importa 2.000+ iconos aunque solo uses 5
import * as Icons from 'simple-icons';
const icono = Icons.siReact;La forma correcta:
// Solo lo que necesitas
import { siReact, siTypescript, siNextdotjs } from 'simple-icons';// Solo lo que necesitas
import { siReact, siTypescript, siNextdotjs } from 'simple-icons';Y ya que estamos, usad code splitting:
// Los componentes se cargan solo cuando se necesitan
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Ajustes = React.lazy(() => import('./pages/Ajustes'));
function App() {
return (
<Suspense fallback={<p>Cargando...</p>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/ajustes" element={<Ajustes />} />
</Routes>
</Suspense>
);
}// Los componentes se cargan solo cuando se necesitan
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Ajustes = React.lazy(() => import('./pages/Ajustes'));
function App() {
return (
<Suspense fallback={<p>Cargando...</p>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/ajustes" element={<Ajustes />} />
</Routes>
</Suspense>
);
}Prioridad ALTA: Deja de re-renderizar
El despiste típico:
// Ordena el array EN CADA RENDER
function ListaProductos({ productos }) {
const ordenados = productos.sort((a, b) => a.precio - b.precio);
return <Lista items={ordenados} />;
}// Ordena el array EN CADA RENDER
function ListaProductos({ productos }) {
const ordenados = productos.sort((a, b) => a.precio - b.precio);
return <Lista items={ordenados} />;
}La versión que no quema la CPU:
// Solo ordena cuando 'productos' cambia
function ListaProductos({ productos }) {
const ordenados = useMemo(
() => [...productos].sort((a, b) => a.precio - b.precio),
[productos]
);
return <Lista items={ordenados} />;
}// Solo ordena cuando 'productos' cambia
function ListaProductos({ productos }) {
const ordenados = useMemo(
() => [...productos].sort((a, b) => a.precio - b.precio),
[productos]
);
return <Lista items={ordenados} />;
}Otro clásico — parsear JSON en cada render:
// Parsea JSON CADA VEZ que el componente renderiza
const [config, setConfig] = useState(
JSON.parse(localStorage.getItem('config'))
);
// Parsea UNA VEZ durante la inicialización
const [config, setConfig] = useState(() =>
JSON.parse(localStorage.getItem('config'))
);// Parsea JSON CADA VEZ que el componente renderiza
const [config, setConfig] = useState(
JSON.parse(localStorage.getItem('config'))
);
// Parsea UNA VEZ durante la inicialización
const [config, setConfig] = useState(() =>
JSON.parse(localStorage.getItem('config'))
);Esa pequeña flecha () => os puede ahorrar muchos disgustos.
Prioridad MEDIA: Listas largas = Virtual Scrolling
Si tenéis una lista con más de 1000 elementos y renderizáis todos a la vez, vuestra app irá más lenta que un domingo por la tarde.
import { FixedSizeList } from 'react-window';
function ListaVirtualizada({ items }) {
const Fila = ({ index, style }) => (
<div style={style}>
{items[index].nombre}
</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Fila}
</FixedSizeList>
);
}import { FixedSizeList } from 'react-window';
function ListaVirtualizada({ items }) {
const Fila = ({ index, style }) => (
<div style={style}>
{items[index].nombre}
</div>
);
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Fila}
</FixedSizeList>
);
}Solo renderiza lo que el usuario puede ver. El resto ni existe hasta que hace scroll.
El flujo de trabajo que os va a cambiar la vida
Paso 1: Planificar antes de codificar (revolucionario, ¿eh?)
Prompt:
"Necesito implementar [feature]. ANTES de escribir código, quiero que planifiques:
1. Arquitectura del componente
2. Posibles problemas de seguridad
3. Oportunidades de optimización
4. Cómo lo testearíamos
NO escribas código todavía. Solo el plan."Prompt:
"Necesito implementar [feature]. ANTES de escribir código, quiero que planifiques:
1. Arquitectura del componente
2. Posibles problemas de seguridad
3. Oportunidades de optimización
4. Cómo lo testearíamos
NO escribas código todavía. Solo el plan."Esto parece una tontería, pero la diferencia es brutal. Es como pedirle direcciones a alguien vs. que te acompañe.
Paso 2: El truco del /clear
Cada vez que empecéis algo nuevo, usad /clear. No necesitáis el historial de hace 20 prompts consumiendo tokens y confundiendo al modelo.
Paso 3: Automatizad las reviews
Instalad la GitHub App de Claude Code:
/install-github-app/install-github-appY configurad claude-code-review.yml así:
direct_prompt: |
Revisa este PR buscando:
1. Bugs y errores de lógica
2. Vulnerabilidades de seguridad
3. Problemas de rendimiento
Sé conciso. No me cuentes tu vida.direct_prompt: |
Revisa este PR buscando:
1. Bugs y errores de lógica
2. Vulnerabilidades de seguridad
3. Problemas de rendimiento
Sé conciso. No me cuentes tu vida.Los humanos pillan errores de nombres de variables. Claude pilla bugs de lógica y agujeros de seguridad. Es un buen tándem.
Checklist final (imprímelo y pégalo en la pared)
Seguridad:
□ Skills de seguridad instalados
□ Inputs sanitizados (DOMPurify es tu amigo)
□ Protección CSRF en formularios
□ Cero credenciales en el código
□ HttpOnly cookies para tokens
Rendimiento:
□ Sin waterfalls (Promise.all es tu nuevo mejor amigo)
□ Bundle < 200KB (gzipped)
□ Code splitting por rutas
□ Lazy loading para lo que está "below the fold"
□ useMemo/useCallback donde toque
□ Virtual scrolling para listas largas
Core Web Vitals:
□ LCP < 2.5s
□ FID < 100ms
□ CLS < 0.1Seguridad:
□ Skills de seguridad instalados
□ Inputs sanitizados (DOMPurify es tu amigo)
□ Protección CSRF en formularios
□ Cero credenciales en el código
□ HttpOnly cookies para tokens
Rendimiento:
□ Sin waterfalls (Promise.all es tu nuevo mejor amigo)
□ Bundle < 200KB (gzipped)
□ Code splitting por rutas
□ Lazy loading para lo que está "below the fold"
□ useMemo/useCallback donde toque
□ Virtual scrolling para listas largas
Core Web Vitals:
□ LCP < 2.5s
□ FID < 100ms
□ CLS < 0.1Conclusión (o el TLDR para los vagos)
Mira, integrar React con Claude Code no va de escribir código más rápido. Va de escribir mejor código más rápido. La diferencia es importante.
Las claves:
- Instala los skills de seguridad YA — No mañana, no "cuando tenga tiempo". Ahora.
- Prioriza por impacto — Arreglar waterfalls te dará más que micro-optimizar useMemos.
- Planifica antes de implementar — 5 minutos de planificación ahorran 5 horas de debugging.
- Automatiza las reviews — Los humanos somos buenos en algunas cosas. Revisar código a las 6 de la tarde de un viernes no es una de ellas.
- No delegues, colabora — Claude Code es un compañero, no un sustituto. Tú sigues siendo el responsable de lo que acabas subiendo a producción.
El futuro del desarrollo no es elegir entre velocidad y calidad. Es tener ambas. Y ahora tenéis las herramientas para conseguirlo.
Ahora cerrad Medium y poneos a currar, anda.