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:
parent
dcc2151530
commit
d80f1947f5
219
docs/SOLUCION_DEFINITIVA_USELAYOUTEFFECT.md
Normal file
219
docs/SOLUCION_DEFINITIVA_USELAYOUTEFFECT.md
Normal 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)
|
||||||
|
|
||||||
|
|
@ -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
94
scripts/diagnose-react.js
Executable 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');
|
||||||
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue