In debugging React (or any kind of JavaScript-based application) developers tend to rely on the standard console.log
for insights. However, if you’re looking to detect and prevent bugs in React? There is more to it than just using console.log
.
React comes with a series of tools out of the box that can help you find and eliminate those pesky bugs in your application. Before we get to know them, let’s take a look at the types of bugs you are likely to come across.
Debugging is a series of steps taken to remove a bug from an application. Bugs are things that break the code or make it work in a way that results in an unintended side effect.
In React, knowing what kind of bug you’re dealing with can help you select the right debugging process. There are four general categories of bugs that you’ll most likely encounter in a React app:
A user interface bug in React occurs when something is not doing what we expect it to do, or when parts of the UI just don’t look quite right. The cause of this bug is often caused by one of the following things:
By design, React repaints the interface every time a change occurs. For example, a common one is that your form inputs don’t seem to be working quite right. The value displayed is not what you typed in. When this happens, it’s because you haven’t specifically called the individual set
and get
values.
//setting up the state const [search, setSearch] = useState(''); ... //the jsx for setting and using the state <input className="search-bar" type="text" value={search} onChange={updateSearch} />
If the placement of UI elements and its visual appearance doesn’t seem quite right? Then your CSS is most likely what is broken.
A logical bug occurs when the application is not behaving the way you expect it to behave. Your data may not be rendered correctly or perhaps your interpolation just isn’t working. In this case, debugging becomes a process of narrowing down the origin of the issue so you can inspect and eliminate the bug accordingly.
Networking bugs are often tricky because you’re dealing with a third-party service like an API. When a network times out, it’s usually because you’ve reached the API limits, your keys are invalid or you’re experiencing a CORs issue because the third-party hasn’t configured the accessibility to accept your requests.
This means your logic is generally correct but something on the other side is returning an error.
A feature regression is when something used to work in the past but no longer does. This can be caused by several factors such as legacy code, conflicts with new code, newly introduced network bugs, or depreciated features that are no longer supported.
When this occurs, it is a matter of updating the code to fit with the current requirements, or refactoring it to ensure future and forward compatibility.
When we try to debug something, we often just use console.log
. However, there’s more to it than just console.log. console
as an object, and .log
is only one of its available methods.
Here is a quick rundown on all the different console statements that you can use and when you should use them.
When using console.warn()
, the log gets highlighted with a yellow background and the text becomes a mustard color.
The data attached to this log includes information such as where the console.warn()
is, when it is invoked, and any callbacks that are attached to it.
console.warn()
gives a red warning highlight, as well as information such as the location where it is invoked, what kind of function the invoked is (eg. the function might be an async function), and any kind of callbacks the function is expected.
When dealing with data, it can be quite hard to trace through reams of nested dropdowns. console.table()
lets you format the data in a way that is easily digestible by presenting it as an actual data table. This can help speed up the debugging process by making the data easier to read.
console.trace()
works in a similar fashion to error
and warn
, except the highlighting is not as dramatic.
It is presented with all the related information up to the nearest scope of its invoked space such as function name, type of function, and any kind of attached special features such as guards, and callbacks.
You can create debugging breakpoints in React through your browser by using the debugger;
statement inside your code.
For example, you might have a problematic function but you’re not sure if it’s the one causing the issue. You can place debugger;
in the code and when your React app runs, your browser will automatically stop the process for you to observe and decide if it is the issue.
This can be useful in conjunction with a console
statement. This lets you programmatically pause the application and run it to the next breakpoint when you’re ready to continue.
Here is an example of how it can be used in your React app:
const [recipes, setRecipes] = useState([]); const [search, setSearch] = useState(''); const [query, setQuery] = useState('chicken'); useEffect( () =>{ getRecipes(); debugger; }, [query]) const getRecipes = async () => { const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`); const data = await response.json(); setRecipes(data.hits); console.log(data.hits); debugger; } const updateSearch = e => { setSearch(e.target.value); console.log(search); debugger; } const getSearch = e => { e.preventDefault(); setQuery(search); setSearch(''); }
There are three different debugger stop points in the code above. This means that your application will stop when the debugger;
is invoked and will not proceed to the next breakpoint until you say so.
In addition to using console statements and debugger;
, you can also download React Developer Tools to help you understand how your app is working and what is happening behind the scenes. Once installed, you’ll have access to a React tab inside your browser console.
The React DevTools lets you inspect the React element tree and see how the structure is constructed, including any properties passed in and any related hooks involved.
React DevTools is available as a browser extension for Chrome, Firefox, and Microsoft Edge. Here is a Chrome screenshot of what it looks like:
An error boundary is a visual logging tool that lets you ringfence issues with ‘something went wrong’ messages for your users. This can also be useful for developers too. That is because it lets you log the issue out in the console and silently fails that particular widget without impacting the rest of the app’s functionality.
The error boundary is used to wrap around a particular widget. While it can be a fallback for critical widgets in the production stage, you can also use it in development as a debugging console output tool.
To use an error boundary, you’ll need to set it up as a class like this:
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } }
Once you’ve done that, you can now use it to wrap around your problem widget like this:
<ErrorBoundary> <YourWidget /> </ErrorBoundary>
The console debugging techniques listed here should cover most of your console errors and UI problems you will encounter with React.
While there is no one-size-fits-all fix for your bugs, keeping calm and methodologically going through the issue via a process of elimination will often lead you to arrive at your solution eventually.