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> ); }