We all surely aim for our applications to be bug-proof. A part of it is covering your application with tests. Also, Airbnb claims that 38% of their bugs could have been prevented by TypeScript. We are not going to avoid all issues, unfortunately. A part of our job is to prepare for that and React gives us a few tools to help us.
Strict mode
The strict mode provided by React highlights potential issues within our application. You can add it to any part of your app.
1 2 3 4 5 6 7 8 9 10 |
import React, { StrictMode } from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <StrictMode> <App/> </StrictMode>, document.getElementById('root') ); |
The strict mode can come in handy when working on an application that uses a version of React that is far from new. Over time, certain practices and approaches deprecate. What seems fitting for your React version might not be recommended nowadays. It might even break your application if you update.
Deprecated features that cause warnings
A good example are lifecycles. This issue is so significant that you can see warnings even without using the strict mode.
As you can see in the warning above, lifecycles such as componentWillMount are going to work only with versions prior to 17.x. You can change that by renaming it to UNSAFE_componentWillMount. Thanks to that, the warning is gone. The above, on the other hand, causes a different warning in the strict mode!
Other lifecycles considered legacy are componentWillUpdate and componentWillReceiveProps.
If you want to read more about lifecycle in React, checkout a very comprehensive article by Bartosz Szczeciński
Some of the features connected to the refs were also deprecated. An example of that is the findDOMNode function. After turning the strict mode on, the warning message tells you precisely where the problematic piece of code is located.
App.js
1 2 3 4 |
componentDidMount() { const ref = ReactDOM.findDOMNode(this); ref.focus(); } |
Another feature that is now deprecated is the usage of string refs.
1 |
<div ref="referenceName" /> |
Check out the docs if you want to know more about the refs
Because the legacy context API is likely to be removed from the next major version, it also causes warnings in the strict mode.
All the warnings are helpful and informative, and they propose how to approach the issue.
Dealing with unexpected side effects
When your users interact with the application, some lifecycle methods might invoke multiple times.
It is important not to cause any side-effects in the lifecycle functions invoked in the render phase of React. Unfortunately, the strict mode can’t automatically detect it. In an attempt to make it more deterministic, it double-invokes the following methods:
- constructor()
- render()
- setState updater functions
- getDerivedStateFromProps()
The above behavior happens only in the development mode, so you don’t have to worry about decreasing the user experience by slowing your application down.
Error boundaries
All of the above aims to help us avoid errors in the first place. We can’t assume that we’ve dealt with every possible issue though. We need to prepare for the worst!
The concept of Error Boundaries can be a great help in achieving that. It is a component that uses the componentDidCatch lifecycle method, and it works similar to the try…catch but is intended for components. It catches errors that happen in its children and receives the error object and the stack trace.
index.js
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import ErrorBoundary from './ErrorBoundary/ErrorBoundary'; ReactDOM.render( <ErrorBoundary> <App/> </ErrorBoundary>, document.getElementById('root') ); |
ErrorBoundary.js
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React, { Component } from 'react'; class ErrorBoundary extends Component { componentDidCatch(error, stackTrace) { console.error('Something bad happened!', error, stackTrace); } render() { return this.props.children; } } export default ErrorBoundary; |
App.js
1 2 3 4 5 6 7 8 9 10 11 12 |
import React from 'react'; const App = () => { throw new Error(':('); return ( <div> Hello world! </div> ); } export default App; |
Now, as soon as the App component throws an error, the componentDidCatch function invokes. The code above results in a blank page when we run it on production, which can be very confusing for users.
With the help of the getDerivedStateFromError function, we can create a custom error page. It is a lifecycle function that React invokes after a descendant component throws an error and receives the thrown error as an argument. It should end by returning a value to update the state of the component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import React, { Component } from 'react'; class ErrorBoundary extends Component { state = { error: null }; static getDerivedStateFromError(error) { return { error: error.message }; } componentDidCatch(error, errorInfo) { console.error('Something bad happened!', error, errorInfo); } render() { if (this.state.error) { return ( <div> <h1>Something went wrong :(</h1> <p> { this.state.error } </p> </div> ) } return this.props.children; } } export default ErrorBoundary; |
Now, every time our ErrorBoundary caches an error resulting in unmounting the React component tree, a fallback message is displayed. A crucial note is that the error boundaries don’t catch all types of errors. You should handle asynchronous code and event handles separately by using try…catch.
Also, our error boundary won’t catch errors thrown in the error boundary itself.
You can create multiple error boundaries and place them anywhere you want. This way, you can have different behavior depending on the part of the application that causes the error.
Summary
Today we’ve covered some ways of making our applications more resistant to breaking up due to errors. Thanks to the strict mode, we can identify potential issues within our application that might cause errors, especially when updating React. By adding error boundaries, we can handle issues if we fail to prevent some of them. Implementing both of the above approaches can improve your confidence in providing the best possible experience for your users.