Uno de los principales problemas de trabajar con React es controlar adecuadamente los errores, ya que cuando se produce un error no controlado React desmonta en cascada todos los componentes padres hasta encontrar un componte que controle la excepción, sin embargo, al no tener un componente que controle los errores provoca que toda la aplicación sea desmontada en cascada hasta:
En la imagen anterior podemos ver claramente el problema que acabamos de mencionar, pues en este ejemplo, un error se produce en el componente rojo de la parte inferior, lo que provoca al no se controlado el error, se comience a desmontar todos los componentes en cascada, lo que provoca eventualmente que toda la aplicación se desmonte, pues llegaremos al componente padre de toda la aplicación.
Para evitar este problema es necesario identificar los componentes que son potencialmente desastrosos o que existe alguna posibilidad de que fallen por algún motivo, por ejemplo, cuando recupera datos del servidor o cuando hace algún algún renderizado muy complejo y agregarle un control de errores, para esto, React proporciona el método componentDidCatch
.
El nombre de este método no es aleatorio, si no que corresponde a los método del ciclo de vida de los componentes, y será ejecutado por React cuando un componente falle:
import React from 'react'
export default class DangerousComponent extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, errorInfo) {
this.setState({
hasError: true
})
}
render() {
if (this.state.hasError) {
return <h1>Ups... Something went wrong.</h1>;
}else{
return (<h1>Welcome to mi page</h1>)
}
}
}
El ejemplo anterior muestra un componente capas de controlar sus propios errores, ya que define el método componentDidCatch
, el cual se ejecutará únicamente cuando se produzca un error durante el montaje o la actualización del componente.
Podrás ver que el componente anterior inicializa su estado con la propiedad hasError: false
la cual es utiliza en el método render
para determinar que mostrarle al usuario. Cuando esta propiedad está en false
le indica que no ha ningún error en la página, por lo que podemos mostrar cualquier componente o lo que queremos que el usuario vea normalmente, sin embargo, cuando un error se produce, se ejecuta el método componentDidCatch
y actualiza esta propiedad a false
, lo que provoca dos cosas, detectará que el error ha sido controlado deteniendo el desmontaje del componente y en su lugar, lo actualizará con el componente con el nuevo estado establecido en este mismo método, sin embargo, ahora la propiedad hasError
valdrá true
lo que hará que el método render
produzca un renderizado diferente, con lo que podemos personalizar como el error se mostrado al usuario en lugar de que toda la aplicación sea desmontada.
Implementando un Error Boundary
A pesar de que la estrategia definida anteriormente sea la más obvia, no es la más recomendada, pues definimos en el mismo componente como deberá renderizarse normalmente y en caso de error, lo que provocaría que en el mismo componente implementemos doble lógica, lo que aumenta su complejidad y reduce su capacidad de se reutilizable.
¿Recuerda que dijimos que React desmonta en cascada todos los componentes hasta que encuentra un componente que controle el error? pues bien, podemos aprovechar esta capacidad para crear un componente especial que su única responsabilidad sea la de controlar el error, por lo cual deberá ser un componente que contenga al componente que puede fallar, de esta forma, el error se propagará hasta el componente encargado de controlar el error, por lo que la aplicación solo se desmontará parcialmente y no toda.
import React from 'react'
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, errorInfo) {
this.setState({
hasError: true
})
}
render() {
if (this.state.hasError) {
// Render error message or component
return <ErrorComponent/>
}
return this.props.children;
}
}
El componente anterior es muy similar al que vimos hace un momento, pero tiene una gran diferencia. Si observamos el método render
podrás observar que en caso de que no exista un error, este renderizará los componentes enviados como hijos (childrens), en lugar de un componte fijo.
Ahora bien, la pregunta es como utilizamos este nuevo componente en nuestra aplicación; pues la respuesta es simple:
export default App extends React.Component{
render(){
return(
<ErrorBoundary>
<DangerousComponent/>
<ErrorBoundary/>
)
}
}
En este nuevo componente podemos ver como hemos declarado el componente DangerousComponent
dentro del componente ErrorBoundary
, lo que provocará que cuando el componente DangerousComponent
falle, se desmonte y propague el error a al padre, en este caso, al ErrorBoundary
, el cual se encargará de controlar error.
En esta nueva imagen podemos ver como ahora el error se propago solo hasta el componente ErrorBoundary
en lugar de desmontar toda la aplicación.
Finalmente, es importante resaltar que el ErrorBoundary
pueda estar en cualquier nivel de la aplicación, y el nivel de este determinará hasta que punto la aplicación será desmontada.
Conclusiones
El manejo de errores es algo que normalmente se pasa por alto por los desarrolladores, sin embargo, es algo que deberíamos de tomar más en cuenta, pues React no es tolerante a los fallos y dará como resultado el desmontaje completo de la aplicación, lo que sin duda, dará una pésima experiencia al usuario.
Si quieres aprender más sobre React, te invitamos a ver nuestro curso de Mastering React donde aprenderás de los simple a lo avanzado en un curso súper completo.