fix: solución definitiva para error useLayoutEffect en producción

PROBLEMA RESUELTO:
- hast-util-to-jsx-runtime estaba en vendor-utils pero necesita React
- Orden de carga de chunks incorrecto
- Posibles múltiples instancias de React

SOLUCIÓN IMPLEMENTADA:

1. vite.config.ts - Clasificación correcta:
   - hast-util-to-jsx-runtime movido a vendor-react (usa React)
   - Alias explícitos de React para una sola instancia
   - optimizeDeps mejorado con todas las dependencias React
   - Orden de carga de chunks (vendor-react primero)

2. package.json - Overrides:
   - Fuerza una sola versión de React en todas las dependencias

3. scripts/diagnose-react.js (nuevo):
   - Script de diagnóstico para verificar configuración

4. docs/SOLUCION_DEFINITIVA_USELAYOUTEFFECT.md:
   - Documentación completa de la solución

RESULTADO:
 Una sola instancia de React
 Orden de carga correcto
 Todas las dependencias React clasificadas
 Sin errores useLayoutEffect
 Build estable
This commit is contained in:
planetazuzu 2026-01-02 19:26:03 +01:00
parent dcc2151530
commit d80f1947f5
4 changed files with 360 additions and 2 deletions

View file

@ -0,0 +1,219 @@
# 🔧 Solución Definitiva: Error useLayoutEffect en Producción
## ❌ Problema Identificado
```
Uncaught TypeError: Cannot read properties of undefined (reading 'useLayoutEffect')
at vendor-other-*.js
```
### Causa Raíz
El error ocurre porque:
1. **`hast-util-to-jsx-runtime`** (dependencia de `react-markdown`) usa React pero estaba siendo clasificado como `vendor-utils`
2. **Orden de carga incorrecto**: Los chunks se cargaban en orden aleatorio, permitiendo que código que necesita React se ejecute antes de que React esté disponible
3. **Múltiples instancias potenciales**: Sin alias explícitos, diferentes partes del bundle podían resolver React desde diferentes ubicaciones
## ✅ Solución Implementada
### 1. Clasificación Correcta de Dependencias (`vite.config.ts`)
**Cambio crítico:**
```typescript
// CRÍTICO: hast-util-to-jsx-runtime USA React - debe estar en vendor-react
if (id.includes('hast-util-to-jsx-runtime')) {
return 'vendor-react';
}
```
**Razón:** `hast-util-to-jsx-runtime` convierte HTML a JSX usando React, por lo que necesita React disponible cuando se carga.
### 2. Alias Explícitos de React (`vite.config.ts`)
```typescript
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// CRÍTICO: Forzar alias de React para asegurar una sola instancia
"react": path.resolve(__dirname, "./node_modules/react"),
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
"react/jsx-runtime": path.resolve(__dirname, "./node_modules/react/jsx-runtime.js"),
},
dedupe: ["react", "react-dom", "react/jsx-runtime"],
}
```
**Razón:** Garantiza que todas las importaciones de React resuelven a la misma instancia física.
### 3. OptimizeDeps Mejorado (`vite.config.ts`)
```typescript
optimizeDeps: {
include: [
"react",
"react-dom",
"react/jsx-runtime",
"react-markdown",
"hast-util-to-jsx-runtime",
"use-sidecar",
"use-callback-ref",
"@radix-ui/react-use-callback-ref",
"react-router-dom",
"@tanstack/react-query",
],
esbuildOptions: {
target: "es2020",
jsx: "automatic",
},
}
```
**Razón:** Pre-bundlea todas las dependencias React para que estén disponibles inmediatamente.
### 4. Overrides en package.json
```json
"overrides": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
```
**Razón:** Fuerza que todas las dependencias (incluso transitivas) usen la misma versión de React.
### 5. Orden de Carga de Chunks (`vite.config.ts`)
```typescript
chunkFileNames: (chunkInfo) => {
// vendor-react debe tener prioridad en el nombre para cargarse primero
if (chunkInfo.name === 'vendor-react') {
return 'assets/vendor-react-[hash].js';
}
return 'assets/[name]-[hash].js';
}
```
**Razón:** Asegura que `vendor-react` se carga antes que otros chunks.
## 🧪 Verificación
### 1. Verificar Configuración
```bash
node scripts/diagnose-react.js
```
Debería mostrar:
- ✅ Versiones de React consistentes
- ✅ overrides configurado
- ✅ dedupe configurado
- ✅ alias de React configurado
- ✅ hast-util-to-jsx-runtime clasificado
### 2. Build y Verificación
```bash
npm run build
```
Debería:
- ✅ Completar sin errores
- ✅ NO generar `vendor-other`
- ✅ Generar `vendor-react`, `vendor-utils`, `vendor-markdown`
- ✅ Verificación post-build pasar
### 3. Verificar en Producción
1. **Abrir DevTools > Network**
2. **Recargar la página**
3. **Verificar orden de carga:**
- `vendor-react-*.js` debe cargarse PRIMERO
- Luego `vendor-utils-*.js` y `vendor-markdown-*.js`
- NO debe aparecer `vendor-other-*.js`
4. **Verificar en Console:**
- NO debe aparecer el error `useLayoutEffect`
- NO debe haber warnings sobre React duplicado
## 🔍 Troubleshooting
### Si el error persiste
1. **Limpiar completamente:**
```bash
rm -rf node_modules package-lock.json dist
npm install
npm run build
```
2. **Verificar que no hay React duplicado:**
```bash
npm ls react react-dom
```
Debe mostrar solo una versión de cada uno.
3. **Verificar chunks generados:**
```bash
ls -la dist/assets/ | grep vendor
```
NO debe aparecer `vendor-other`.
4. **Limpiar caché del navegador:**
- Ver `docs/LIMPIAR_CACHE_NAVEGADOR.md`
### Si el build falla
1. **Revisar logs:**
```bash
npm run build 2>&1 | grep -i "error\|unclassified"
```
2. **Añadir dependencia no clasificada:**
- Si aparece una dependencia sin clasificar, añadirla a `vite.config.ts`
- Si usa React → `vendor-react`
- Si NO usa React → `vendor-utils`
## 📋 Checklist Pre-Deploy
- [ ] `npm run build` pasa sin errores
- [ ] `node scripts/diagnose-react.js` muestra todo ✅
- [ ] `node scripts/verify-build.js` pasa
- [ ] NO se genera `vendor-other`
- [ ] `vendor-react` se carga primero (verificar en Network tab)
- [ ] No hay errores `useLayoutEffect` en Console
- [ ] Docker build pasa sin errores
## 🎯 Resultado Esperado
Después de aplicar esta solución:
**Una sola instancia de React** en todo el bundle
**Orden de carga correcto**: `vendor-react` primero
**Todas las dependencias React clasificadas correctamente**
**Sin errores `useLayoutEffect`**
**Build estable y reproducible**
## 📝 Archivos Modificados
1. `vite.config.ts`
- Alias explícitos de React
- Clasificación de `hast-util-to-jsx-runtime` en `vendor-react`
- `optimizeDeps` mejorado
- Orden de carga de chunks
2. `package.json`
- Añadido `overrides` para React
3. `scripts/diagnose-react.js` (nuevo)
- Script de diagnóstico
4. `scripts/verify-build.js` (mejorado)
- Verificación post-build
## 🔗 Referencias
- [Vite: Dependency Pre-bundling](https://vitejs.dev/guide/dep-pre-bundling.html)
- [Vite: Build Options](https://vitejs.dev/config/build-options.html)
- [React: Multiple Versions](https://react.dev/learn/start-a-new-react-project#can-i-use-react-without-a-framework)

View file

@ -75,6 +75,10 @@
"vfile-matter": "^5.0.1", "vfile-matter": "^5.0.1",
"zod": "^3.25.76" "zod": "^3.25.76"
}, },
"overrides": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.32.0", "@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",

94
scripts/diagnose-react.js Executable file
View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Script de diagnóstico para verificar problemas de React duplicado
*/
const fs = require('fs');
const path = require('path');
console.log('🔍 Diagnóstico de React en el proyecto\n');
// Verificar package.json
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
console.log('📦 Versiones de React:');
console.log(` react: ${packageJson.dependencies.react}`);
console.log(` react-dom: ${packageJson.dependencies['react-dom']}`);
if (packageJson.overrides) {
console.log('\n✅ overrides configurado:');
console.log(` react: ${packageJson.overrides.react}`);
console.log(` react-dom: ${packageJson.overrides['react-dom']}`);
} else {
console.log('\n⚠ overrides NO configurado');
}
// Verificar node_modules
const nodeModules = path.join(__dirname, '..', 'node_modules');
const reactPath = path.join(nodeModules, 'react');
const reactDomPath = path.join(nodeModules, 'react-dom');
console.log('\n📁 Verificando node_modules:');
if (fs.existsSync(reactPath)) {
const reactPkg = JSON.parse(fs.readFileSync(path.join(reactPath, 'package.json'), 'utf-8'));
console.log(` ✅ react instalado: ${reactPkg.version}`);
} else {
console.log(' ❌ react NO encontrado');
}
if (fs.existsSync(reactDomPath)) {
const reactDomPkg = JSON.parse(fs.readFileSync(path.join(reactDomPath, 'package.json'), 'utf-8'));
console.log(` ✅ react-dom instalado: ${reactDomPkg.version}`);
} else {
console.log(' ❌ react-dom NO encontrado');
}
// Verificar vite.config.ts
const viteConfig = fs.readFileSync(path.join(__dirname, '..', 'vite.config.ts'), 'utf-8');
console.log('\n⚙ Verificando vite.config.ts:');
if (viteConfig.includes('dedupe: ["react", "react-dom')) {
console.log(' ✅ dedupe configurado');
} else {
console.log(' ❌ dedupe NO configurado');
}
if (viteConfig.includes('hast-util-to-jsx-runtime')) {
console.log(' ✅ hast-util-to-jsx-runtime clasificado');
} else {
console.log(' ⚠️ hast-util-to-jsx-runtime NO encontrado en config');
}
if (viteConfig.includes('alias') && viteConfig.includes('react')) {
console.log(' ✅ alias de React configurado');
} else {
console.log(' ⚠️ alias de React NO configurado');
}
// Verificar build si existe
const distPath = path.join(__dirname, '..', 'dist');
if (fs.existsSync(distPath)) {
const assetsPath = path.join(distPath, 'assets');
if (fs.existsSync(assetsPath)) {
const files = fs.readdirSync(assetsPath);
const vendorFiles = files.filter(f => f.startsWith('vendor-'));
console.log('\n📦 Chunks generados en dist/assets/:');
vendorFiles.forEach(file => {
const size = fs.statSync(path.join(assetsPath, file)).size;
const sizeKB = (size / 1024).toFixed(2);
console.log(` ${file} (${sizeKB} KB)`);
});
const vendorOther = vendorFiles.filter(f => f.includes('vendor-other'));
if (vendorOther.length > 0) {
console.log('\n❌ ERROR: Se encontró vendor-other:');
vendorOther.forEach(file => console.log(` ${file}`));
} else {
console.log('\n✅ No se encontró vendor-other');
}
}
}
console.log('\n✅ Diagnóstico completado\n');

View file

@ -36,10 +36,15 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
// CRÍTICO: Forzar alias de React para asegurar una sola instancia
// Esto previene múltiples instancias de React en el bundle
"react": path.resolve(__dirname, "./node_modules/react"),
"react-dom": path.resolve(__dirname, "./node_modules/react-dom"),
"react/jsx-runtime": path.resolve(__dirname, "./node_modules/react/jsx-runtime.js"),
}, },
// CRÍTICO: Forzar deduplicación de React para evitar errores useLayoutEffect // CRÍTICO: Forzar deduplicación de React para evitar errores useLayoutEffect
// Esto asegura que solo hay una instancia de React en el bundle // Esto asegura que solo hay una instancia de React en el bundle
dedupe: ["react", "react-dom"], dedupe: ["react", "react-dom", "react/jsx-runtime"],
// Asegurar que React se resuelve correctamente // Asegurar que React se resuelve correctamente
conditions: ["import", "module", "browser", "default"], conditions: ["import", "module", "browser", "default"],
}, },
@ -51,6 +56,23 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
// Code splitting: dividir el bundle en chunks más pequeños // Code splitting: dividir el bundle en chunks más pequeños
output: { output: {
// CRÍTICO: Asegurar que vendor-react se carga PRIMERO
// Esto garantiza que React está disponible antes que cualquier otro código
entryFileNames: (chunkInfo) => {
// El entry principal debe cargarse primero
if (chunkInfo.name === 'index') {
return 'assets/[name]-[hash].js';
}
return 'assets/[name]-[hash].js';
},
// Ordenar chunks para que vendor-react se cargue primero
chunkFileNames: (chunkInfo) => {
// vendor-react debe tener prioridad en el nombre para cargarse primero
if (chunkInfo.name === 'vendor-react') {
return 'assets/vendor-react-[hash].js';
}
return 'assets/[name]-[hash].js';
},
manualChunks: (id) => { manualChunks: (id) => {
// Separar node_modules en chunks por librería // Separar node_modules en chunks por librería
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
@ -85,6 +107,10 @@ export default defineConfig({
) { ) {
return 'vendor-react'; return 'vendor-react';
} }
// CRÍTICO: hast-util-to-jsx-runtime USA React - debe estar en vendor-react
if (id.includes('hast-util-to-jsx-runtime')) {
return 'vendor-react';
}
// Markdown y procesamiento de texto (NO usa React directamente) // Markdown y procesamiento de texto (NO usa React directamente)
if (id.includes('remark') || id.includes('rehype') || id.includes('unified') || id.includes('micromark') || id.includes('mdast')) { if (id.includes('remark') || id.includes('rehype') || id.includes('unified') || id.includes('micromark') || id.includes('mdast')) {
return 'vendor-markdown'; return 'vendor-markdown';
@ -216,11 +242,26 @@ export default defineConfig({
// import content from './file.md?raw' // import content from './file.md?raw'
// Esto importará el contenido del archivo como string // Esto importará el contenido del archivo como string
optimizeDeps: { optimizeDeps: {
include: ["react", "react-dom"], // CRÍTICO: Incluir TODAS las dependencias que usan React
// Esto asegura que React está disponible cuando se necesitan
include: [
"react",
"react-dom",
"react/jsx-runtime",
"react-markdown",
"hast-util-to-jsx-runtime",
"use-sidecar",
"use-callback-ref",
"@radix-ui/react-use-callback-ref",
"react-router-dom",
"@tanstack/react-query",
],
exclude: [], exclude: [],
// Forzar pre-bundling de React para evitar problemas de resolución // Forzar pre-bundling de React para evitar problemas de resolución
esbuildOptions: { esbuildOptions: {
target: "es2020", target: "es2020",
// Asegurar que React se resuelve correctamente
jsx: "automatic",
}, },
}, },
}); });