Unraveling the Mystery of Debugging Using React Developer Tools

Expert software developers utilize powerful developer tools to work more efficiently and boost productivity. The significance of these tools becomes even more pronounced when it comes to debugging, often considered the most challenging aspect of software development.

This is where React Developer Tools comes in, a browser extension supported by Meta, the company behind React, and used by 3 million developers globally. We will delve into how this tool can enhance your React debugging skills—from examining components, states, and props to monitoring rendering and performance—all without the complexities of browser console logging.

Utilizing React Developer Tools

Front-end web developers regularly encounter the need to identify the root cause of intricate issues within applications. A common approach involves utilizing numerous console.log statements and reviewing the browser’s console. While this method can be effective, it lacks efficiency. Luckily, React Developer Tools simplifies this process and empowers us to:

  • Visualize the React component tree.
  • Inspect and modify the state/props of any component within the tree.
  • Monitor the rendering time of a component.
  • Determine the reasons behind a component’s re-rendering.

These features enable you to optimize an app, uncover bugs, and pinpoint other problems effortlessly.

Installing the Extension

To begin, let’s add the React Developer Tools extension to your browser by following these six steps. Our focus will be on Chrome, but similar procedures apply to other browsers (Firefox, Edge, etc.):

  1. Navigate to the Chrome plugin page.
  2. Click on Add to Chrome.
  3. Click on Add extension in the pop-up that appears.
  4. Wait for the download to finish.
  5. Click on the extensions (puzzle) icon in the top right corner of your browser.
  6. Click on the pin icon for easy access to the extension.

Now, the icon you pinned in step 6 will change its appearance whenever you visit a website built with React:

Four variations of the React logo. From left to right, a blue logo with a black background (production), a white logo with a black background and yellow warning triangle (outdated React), a white logo with no background (no React), and a white logo with a red background and a black bug (development).

From left to right, these icons indicate when a page:

With the extension set up, let’s create an application for debugging.

Setting up a Test Application

The create-react-app utility tool can effortlessly set up an app in seconds. Ensure you have install Node.js installed, then use your command line to create your app:

1
npx create-react-app app-to-debug

This might take a moment, as it involves initializing the codebase and installing dependencies. Once done, go to the application’s root folder and launch your React app:

1
2
cd app-to-debug
npm start

Once compiled, your app will open in your browser:

A webpage with the URL "localhost:3000" shows the React logo. On the screen, a line of text says “Edit src/App.js and save to reload,” and has a Learn React link beneath it.

The React Developer Tools extension icon now indicates that we’re in the development environment.

Practical Debugging Techniques Using React Developer Tools

Let’s explore the developer tools themselves. Start by opening the developer console (Option + ⌘ + J on Mac or Shift + CTRL + J on Windows/Linux). Among the available tabs (Elements, Console, etc.), we’ll be using the Components tab:

A screenshot displays the same webpage as before on the left, but also shows developer tools on the right of the screen. The developer console displays the contents of the Components tab.

Currently, there’s only one component displayed. This is because our test application has rendered only one component, App (see src/index.js). Click on the component to view its props, the react-dom version in use, and the source file.

Monitoring Component State

Let’s begin with the most frequently used feature: examining and modifying a component’s state. To demonstrate, we’ll make changes to our test project. We’ll replace the React placeholder homepage with a basic login form containing three state elements: a username string, a password string, and a boolean for a “Remember me” setting.

In the src folder, delete App.css, App.test.js, and logo.svg. Then create a new file, LoginForm.js, and add the following code:

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
import { useState } from "react";

const LoginForm = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [rememberMe, setRememberMe] = useState(false);

  return (
    <form
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "8px 0",
        padding: 16,
      }}
    >
      <input
        name="username"
        placeholder="Username"
        type="text"
        value={username}
        onChange={(e) => setUsername(e.currentTarget.value)}
      />
      <input
        name="password"
        placeholder="Password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.currentTarget.value)}
      />

      <div>
        <input
          id="rememberMe"
          name="remember_me"
          type="checkbox"
          checked={rememberMe}
          onChange={() => setRememberMe(!rememberMe)}
        />
        <label htmlFor="rememberMe">Remember me</label>
      </div>

      <input type="submit" name="login" value="Log in" />
    </form>
  );
};

export default LoginForm;

Notice how we declare the component. We’re using a named component (const LoginForm => …) to see its name in the dev tools. Anonymous components would show up as Unknown.

LoginForm will be our debugging target, so let’s render it inside App.js:

1
2
3
4
5
6
7
import LoginForm from "./LoginForm";

const App = () => {
  return <LoginForm />;
};

export default App;

Reload the browser window with the Components tab open. Now, beside the App component, you’ll find the LoginForm component. Clicking on LoginForm reveals all the state items we defined using useState hooks. Since we haven’t interacted with any input fields or checkboxes, we see two empty strings and false:

A screenshot of the Component tab, displaying the app component and its LoginForm on the left, and a tab for LoginForm on the right with the three hooks states.

Enter any text in the username and password fields or toggle the checkbox to observe the values updating in the debugging window:

A screenshot of the login component with an “admin” username and a hidden password on the left, and the Components tab with updated hooks states (“admin,” “StrongPassword,” and false) on the right.

You might notice that the state variables lack names, all appearing as State. This is expected behavior because useState only accepts the value argument ("" or false in our case). React doesn’t inherently know the name of the state item.

However, a utility called useDebugValue partially addresses this. It allows setting display names for custom hooks. For instance, you could set the display name Password for a custom usePassword hook.

Tracking Component Props

We can monitor not only state changes, but also component props. Let’s modify LoginForm further:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const LoginForm = ({ defaultUsername, onSubmit }) => {
  const [username, setUsername] = useState(defaultUsername);
  const [password, setPassword] = useState("");
  const [rememberMe, setRememberMe] = useState(false);

  return (
    <form
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "8px 0",
        padding: 16,
      }}
      onSubmit={onSubmit}
    >
// ...

This adds a defaultUsername property to pre-fill the username on initial load, and an onSubmit property to handle form submission actions. We also need to set default values for these properties in App:

1
2
3
const App = () => {
  return <LoginForm defaultUsername="foo@bar.com" onSubmit={() => {}} />;
};

After saving these changes, reload the page. You’ll now see the props within the Components tab:

The same screenshot as above, with different username/password entries ("foo@bar.com" for the username and a blank password) and added props under the Components tab (defaultUsername and onSubmit).

If you need to check how the component reacts to different state/props, you can do so without changing the code. Simply click on the state/prop value in the Components tab and input the desired value.

Analyzing Application Performance

It’s important to note that tracking props and state is possible through console.log. However, React Developer Tools offers two key advantages:

  • Firstly, relying on console logs becomes impractical when a project grows in scale. Managing numerous logs makes finding specific information difficult.
  • Secondly, monitoring component states and properties is only one aspect. When an application functions correctly but experiences slowness, React Developer Tools can pinpoint performance bottlenecks.

For a general performance overview, React Developer Tools can highlight DOM updates. Click on the gear icon in the top right corner of the Components tab.

This opens a pop-up with four tabs. Select the General tab and check the Highlight updates when components render option. Now, when you type in the password field, you’ll see the form highlighted with a green/yellow frame. The more frequent the updates, the brighter the highlight becomes.

The same screenshot as above, with a pop-up appearing over the Components tab. It displays four tabs (General, Debugging, Components, and Profiler), and shows three options inside the General tab: Theme, Display density, and Highlight updates when components render (which is the selected option). The login component shows a filled password field, and appears highlighted in a yellow frame.

For a more detailed performance breakdown, switch from the Components tab to the Profiler tab (and uncheck the highlight option).

You’ll notice a blue circle in the top left corner of the Profiler tab. This button initiates application profiling. Clicking it will track all state/prop updates. Before proceeding, click the gear icon in the top right corner of the tab and enable Record why each component rendered while profiling. This provides explanations for each update.

A screenshot of the login component, with the Profiler tab and a pop-up opened on the right. The profiler is set to record why each component rendered while profiling, and the “Hide commits” functionality is not activated.

With the configuration done, let’s profile our app. Close the settings and click the blue circle. Start typing in the password field and toggle the “Remember me” checkbox. Click the blue circle again to stop profiling and view the results.

Screenshot of a complete configuration, showing the login component on the left side, and the profiler activated and outputting results on the right. The results state why the component rendered (Hook 2 changed) and list when it was rendered and at what speed (in milliseconds).

The profiling results display itemized updates for the LoginForm component. In our case, it shows nine updates: eight for each character typed in the password field, and one for the “Remember me” checkbox. Clicking on an update reveals why the re-render occurred. For instance, the first render states “Hook 2 changed.”

Looking at the second hook in our LoginForm component:

1
const [password, setPassword] = useState("");

The result aligns with our expectation, as the second hook manages the password state. Clicking the last render would show “Hook 3 changed” because our third hook handles the “Remember me” state.

Inspecting React useReducer and Context

The previous examples illustrate simple scenarios. However, React’s API includes more complex features like Context and useReducer.

Let’s incorporate these into our application. First, we’ll add a file for our context. This context will handle user login logic and provide related information. Create AuthenticationContext.js with the following code:

 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { useCallback, useContext, useReducer } from "react";
import { createContext } from "react";

const initialState = {
  loading: false,
  token: undefined,
  error: undefined,
};

const AuthenticationContext = createContext({
  ...initialState,
  logIn: () => {},
});

const reducer = (state, action) => {
  switch (action.type) {
    case "LOG_IN":
      return { ...state, loading: true };
    case "LOG_IN_SUCCESS":
      return { ...state, loading: false, token: action.token };
    case "LOG_IN_ERROR":
      return { ...state, loading: false, error: action.error };
    default:
      return action;
  }
};

const mockAPICall = async (payload) => ({ token: "TOKEN" });

export const AuthenticationContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const logIn = useCallback(async (payload) => {
    try {
      dispatch({ type: "LOG_IN" });
      const response = await mockAPICall(payload);
      dispatch({ type: "LOG_IN_SUCCESS", token: response.token });
    } catch (error) {
      dispatch({ type: "LOG_IN_ERROR", error });
    }
  }, []);

  return (
    <AuthenticationContext.Provider value={{ ...state, logIn }}>
      {children}
    </AuthenticationContext.Provider>
  );
};

export const useAuthentication = () => useContext(AuthenticationContext);

This context provides the loading status, error, result (token), and the login action (logIn). As you can see in the reducer function, initiating login sets the loading value to true. The token gets updated upon successful response; otherwise, an error is set. We don’t need a separate success status because the presence of a token implies success.

To use these values in our app, update App.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { AuthenticationContextProvider } from "./AuthenticationContext";
import LoginForm from "./LoginForm";

const App = () => {
  return (
    <AuthenticationContextProvider>
      <LoginForm defaultUsername="foo@bar.com" />
    </AuthenticationContextProvider>
  );
};

export default App;

Now, reload the page and go to the Components tab. You’ll see the context in the component tree:

A screenshot displaying the login component on the left, with the Components dev tab on the right. The component tree now shows four nested components, from top to bottom: App, AuthenticationContextProvider, Context.Provider, and LoginForm. AuthenticationContextProvider is selected and shows two hooks, Reducer and Callback.

Two new nodes have appeared: AuthenticationContextProvider and Context.Provider. The first is our custom provider wrapping the application in App.js. It contains the reducer hook with the current state. The second node represents the context itself, showing the value being provided to the component tree:

1
2
3
4
5
6
7
8
{
  value: {
    error: undefined,
    loading: false,
    token: undefined,
    logIn: ƒ () {}
  }
}

To ensure React Developer Tools can track reducer changes and display the actual context state, modify LoginForm.js to use the logIn action as the onSubmit callback:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { useCallback, useState } from "react";
import { useAuthentication } from "./AuthenticationContext";

const LoginForm = ({ defaultUsername }) => {
  const { logIn } = useAuthentication();

  const [username, setUsername] = useState(defaultUsername);
  const [password, setPassword] = useState("");
  const [rememberMe, setRememberMe] = useState(false);

  const onSubmit = useCallback(
    async (e) => {
      e.preventDefault();
      await logIn({ username, password, rememberMe });
    },
    [username, password, rememberMe, logIn]
  );

  return (
// ...

Now, in the browser, click Log in to see the token value (previously undefined) update in Context.Provider’s props.

Additional React Debugging Tools

Debugging React applications extends beyond React Developer Tools. Developers can leverage various utilities and their feature sets to create a customized debugging process.

Why Did You Render

First up is why-did-you-render, a powerful performance analysis tool. While not as straightforward as React Developer Tools, it enhances render monitoring by providing human-readable explanations for each render, including state/prop differences and suggestions for improvement.

Screenshot of why-did-you-render indicating that Child: f was rerendered because the props object changed.

Redux DevTools

Redux is a popular state management library often used with React. To learn more, you can refer to my previous article. In essence, it consists of actions and states. Redux DevTools is a user interface that visually represents the actions triggered in your app and the resulting state changes. Here’s an example of the add-on in action on a Medium webpage:

Screenshot of Redux DevTools inspecting a Medium.com page.

Streamlined Problem-solving for Your Projects

React Developer Tools is a simple yet effective addition to your workflow, simplifying the process of resolving issues. While other tools might be beneficial depending on your needs, React Developer Tools is an excellent starting point.

With your newfound React Developer Tools skills, you’ll become proficient in debugging your next app and be able to navigate any React-based page or application significantly faster than traditional code inspection methods.

The Toptal Engineering Blog editorial team extends its thanks to Imam Harir for reviewing the code samples and technical content presented in this article.

Licensed under CC BY-NC-SA 4.0