Volver a React Intermedio
Error Boundaries y Portales
Error Boundaries
Los error boundaries capturan errores de JavaScript en cualquier lugar del árbol de componentes hijo, los registran y muestran una UI alternativa en lugar de romper toda la aplicación.
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
alError?: (error: Error, info: ErrorInfo) => void;
}
interface Estado {
tieneError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, Estado> {
state: Estado = { tieneError: false, error: null };
static getDerivedStateFromError(error: Error): Estado {
return { tieneError: true, error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.alError?.(error, info);
console.error('ErrorBoundary capturó:', error, info.componentStack);
}
render() {
if (this.state.tieneError) {
return this.props.fallback ?? (
<div className="error-fallback">
<h2>Algo salió mal</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ tieneError: false, error: null })}>
Intentar de nuevo
</button>
</div>
);
}
return this.props.children;
}
}
// Uso
function App() {
return (
<ErrorBoundary
fallback={<p>Error al cargar el dashboard</p>}
alError={(err) => registrarEnSentry(err)}
>
<Dashboard />
</ErrorBoundary>
);
}
Hook useErrorBoundary (react-error-boundary)
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
function ComponenteDatos() {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
obtenerDatos().catch(showBoundary); // Activar el boundary programáticamente
}, []);
return <div>Datos</div>;
}
function App() {
return (
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<button onClick={resetErrorBoundary}>Reintentar: {error.message}</button>
)}
>
<ComponenteDatos />
</ErrorBoundary>
);
}
Portales
Renderiza un componente fuera de su jerarquía DOM padre:
import { createPortal } from 'react-dom';
function Modal({ abierto, alCerrar, children }: {
abierto: boolean;
alCerrar: () => void;
children: React.ReactNode;
}) {
if (!abierto) return null;
return createPortal(
<div className="modal-overlay" onClick={alCerrar}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="close-btn" onClick={alCerrar}>✕</button>
{children}
</div>
</div>,
document.body // Renderiza fuera del árbol React — evita problemas de z-index
);
}
function App() {
const [abierto, setAbierto] = useState(false);
return (
<>
<button onClick={() => setAbierto(true)}>Abrir Modal</button>
<Modal abierto={abierto} alCerrar={() => setAbierto(false)}>
<h2>¡Hola desde un Portal!</h2>
<p>Me renderizo en document.body, no dentro de mi padre.</p>
</Modal>
</>
);
}
Tooltip con Portal
function Tooltip({ texto, children }: { texto: string; children: ReactNode }) {
const [mostrar, setMostrar] = useState(false);
const [pos, setPos] = useState({ x: 0, y: 0 });
const handleMouseEnter = (e: React.MouseEvent) => {
const rect = (e.target as HTMLElement).getBoundingClientRect();
setPos({ x: rect.left + rect.width / 2, y: rect.top - 8 });
setMostrar(true);
};
return (
<span onMouseEnter={handleMouseEnter} onMouseLeave={() => setMostrar(false)}>
{children}
{mostrar && createPortal(
<div
className="tooltip"
style={{ position: 'fixed', left: pos.x, top: pos.y, transform: 'translate(-50%, -100%)' }}
>
{texto}
</div>,
document.body
)}
</span>
);
}