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",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"overrides": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@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: {
|
||||
alias: {
|
||||
"@": 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
|
||||
// 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
|
||||
conditions: ["import", "module", "browser", "default"],
|
||||
},
|
||||
|
|
@ -51,6 +56,23 @@ export default defineConfig({
|
|||
rollupOptions: {
|
||||
// Code splitting: dividir el bundle en chunks más pequeños
|
||||
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) => {
|
||||
// Separar node_modules en chunks por librería
|
||||
if (id.includes('node_modules')) {
|
||||
|
|
@ -85,6 +107,10 @@ export default defineConfig({
|
|||
) {
|
||||
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)
|
||||
if (id.includes('remark') || id.includes('rehype') || id.includes('unified') || id.includes('micromark') || id.includes('mdast')) {
|
||||
return 'vendor-markdown';
|
||||
|
|
@ -216,11 +242,26 @@ export default defineConfig({
|
|||
// import content from './file.md?raw'
|
||||
// Esto importará el contenido del archivo como string
|
||||
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: [],
|
||||
// Forzar pre-bundling de React para evitar problemas de resolución
|
||||
esbuildOptions: {
|
||||
target: "es2020",
|
||||
// Asegurar que React se resuelve correctamente
|
||||
jsx: "automatic",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue