Web Front-end12 minute read

Demystifying Debugging With React Developer Tools

Browser console logging is a hassle. Discover how React Developer Tools and third-party libraries make inspecting components, states, and props and tracking rendering and performance so much easier.
Share
Browser console logging is a hassle. Discover how React Developer Tools and third-party libraries make inspecting components, states, and props and tracking rendering and performance so much easier.

The most experienced software engineers leverage the power of developer tools to save time and increase productivity. The importance of these tools multiplies when the time to debug arrives, as debugging may be the most difficult aspect of software development.

Enter React Developer Tools, a browser extension backed by Meta, the creator of React, and used by 3 million people worldwide. We’ll examine how this tool can elevate your React debugging skills—from inspecting components, states, and props to tracking rendering and performance—all without the hassle of browser console logging.

How to Use React Developer Tools

Web developers must determine the root cause of complex problems in an app on a daily basis. One typical method on the front end is to use multiple console.log statements and check the browser’s console. This approach can work, but it is not particularly efficient. Fortunately, React Developer Tools makes things easier and allows us to:

  • See the React component tree.
  • Check and update the state/props of any component in the tree.
  • Track the time for a component to render.
  • Detect why a component has been re-rendered.

With these features, you should be able to optimize an app, find bugs, or pinpoint other issues without much effort.

Installing the Extension

First, follow these six steps to add the React Developer Tools extension to your browser. We’ll focus on a Chrome setup, but you may follow a similar process for your preferred browser (e.g., Firefox, Edge) if desired:

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

Now, whenever you visit a website that is using React, the extension icon you pinned in step 6 will change its appearance:

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, the icon states shown are used when a page:

With our extension up and running, next we’ll create an application to debug.

Creating a Test Application

The create-react-app utility tool can smoothly bootstrap an app in seconds. As a prerequisite, install Node.js on your machine, then create your app via the command line:

npx create-react-app app-to-debug

The operation might take some time, as it needs to initialize the codebase and install dependencies. As soon as it’s done, go to the application’s root folder and start your new React app:

cd app-to-debug
npm start

When compiled, your app appears 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.

Our React Developer Tools extension icon now indicates that we are working in the development environment.

Practical Debugging Methods With React Developer Tools

Let’s move along and learn more about the actual developer tools. First, open the developer console (Option + ⌘ + J on Mac or Shift + CTRL + J on Windows/Linux). There are multiple tabs available (Elements, Console, etc.). The one we need at this stage is Components:

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.

There is just a single component available at this point. That’s correct because our test application has only one component rendered App (see src/index.js). Click on the component to show its props, the version of react-dom used, and the source file.

Tracking Component State

Let’s start with the feature you’ll be using most of the time: checking/editing the state of a component. To demonstrate this functionality, we will make some changes to the test project. We will replace the React placeholder homepage with a simple login form holding three state pieces: a username string, a password string, and a boolean representing a “Remember me” setting.

In the src folder, remove App.css, App.test.js, and logo.svg, then add a new LoginForm.js file as follows:

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;

Pay attention to the component declaration approach. We’re using the named component (const LoginForm => …) to see its name in the dev tools. Anonymous components are displayed as Unknown.

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

import LoginForm from "./LoginForm";

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

export default App;

Return to the browser window with the Components tab open. Now, next to the App component you’ll see the LoginForm component. Clicking on LoginForm will reveal all the state items we’ve created using useState hooks. Since we haven’t yet entered any text or check box inputs, 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.

Type anything in the username and password fields or click on the check box to see the values in the debugging window update:

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 may have noticed that there are no names for the state variables. All of them are called State. This is the expected behavior of the tool because useState accepts only the value argument ("" or false in our example). React knows nothing about the name of this state item.

A utility called useDebugValue partially solves this problem. It can set the display name for custom hooks. For instance, you can set the display name Password for a custom usePassword hook.

Monitoring Component Props

We can monitor not only state changes, but also component props. We’ll first modify LoginForm:

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}
    >
// ...

The code above will add a defaultUsername property to have a username filled on the initial load, and onSubmit property to control submit form actions. We also must set these properties’ defaults inside App:

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

Reload the page after the changes have been made and you’ll see the props inside 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 will react to a different state/props, you can do it without changing any code. Click on the state/prop value in the Components tab and set the desired value.

Measuring Application Performance

At this point, we should note that tracking props and state is possible via console.log. However, React Developer Tools offers two advantages over this approach:

  • First, logging to the console is unrealistic when scaling a project. The more logs you have the more difficult it is to find what you need.
  • Second, monitoring components’ states and properties is only part of the job. If you run into a case when your application works correctly but is slow, React Developer Tools can identify certain performance issues.

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

You’ll see a pop-up with four tabs. Click on the General tab and select the Highlight updates when components render check box. Start typing in the password field, and you’ll have your form wrapped with a green/yellow frame. The more updates performed per second, the more highlighted the frame 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, we’ll move from the Components tab to the Profiler tab (don’t forget to uncheck the highlight option).

In the Profiler tab, you will see a blue circle in the top left corner. It’s a button to start profiling your application. As soon as you click on it, all state/props updates will be tracked. Before performing this step, however, we will click on the gear icon in the top right corner of the tab and check the Record why each component rendered while profiling check box. It will extend the functionality of the profiler with the updates’ explanations.

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.

The configuration is now complete, so let’s proceed and profile our app. Close the settings overlay and click on the blue circle button. Start typing in the password field and select the “Remember me” box. Then click the circle button again to stop profiling and see 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).

In the profiling results, you should see itemized updates of the LoginForm component. Our example shows nine updates: eight for each character in the password field and one for the “Remember me” check box. If you click on any update, you will have an explanation of why the render happened. For example, the first render says “Hook 2 changed.”

Let’s look at the second hook of the LoginForm component:

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

Our result makes sense since the second hook is responsible for password state management. If you click on the last render, it will show “Hook 3 changed” because our third hook handles the “Remember me” state.

Viewing React useReducer and Context

The examples above provide a glimpse of simple scenarios. However, React’s API includes more complicated features, such as Context and useReducer.

Let’s add them to our application. First, we will have to add a file with the context. The context we’re going to make will be used for logging a user in and providing the information of the login action. We’ll create the AuthenticationContext.js file with the following content:

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 will provide the loading status, error, result (token), and action to perform (logIn) of our authentication logic. As you can see from the reducer function, initiating the login action will set the loading value to true. The token will be updated if the response is successful; otherwise, an error will be set. We won’t add a success status value because this value is available through token (if there is a token, we’ve had a successful operation).

To populate our app with these values, we’ll need to update our App.js file:

import { AuthenticationContextProvider } from "./AuthenticationContext";
import LoginForm from "./LoginForm";

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

export default App;

You can now reload the page, open the Components tab and 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.

There are two nodes added: AuthenticationContextProvider and Context.Provider. The first one is the custom provider we’re using to wrap the application in App.js file. It contains a reducer hook with the current state. The second one is the context representation showing the exact value provided throughout the component tree:

{
  value: {
    error: undefined,
    loading: false,
    token: undefined,
    logIn: ƒ () {}
  }
}

To ensure that React Developer Tools can monitor reducer changes and show the actual state of the context, we’ll adjust the LoginForm.js file to use the logIn action as the onSubmit callback:

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, if you go back to the browser and click Log in, you’ll see that token, which used to be undefined, has an updated value in Context.Provider’s props.

Supplemental React Debugging Tools

Debugging a React application does not stop at React Developer Tools. Engineers can leverage multiple utilities and their mix of features to create the perfect process for their needs.

Why Did You Render

The first tool worth mentioning is a first-class performance analyst, why-did-you-render. It’s not as simple to use as React Developer Tools, but it enhances render monitoring by explaining each render with human-readable text, with state/props differences and ideas on how to fix issues.

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

Redux DevTools

Redux is a well-known state management library used by many React engineers, and you can learn more about it by reading my previous article. In short, it consists of two parts: actions and states. Redux DevTools is a user interface representing actions triggered in your app and the consequent state. You can see the add-on in action on a Medium webpage:

Screenshot of Redux DevTools inspecting a Medium.com page.

Simplified Problem-solving for Your Next App

React Developer Tools is a simple yet powerful addition to your workflow that makes problems easier to fix. Depending on your requirements, you may benefit from additional tools, but React Developer Tools should be your starting point.

With your new React Developer Tools skillset, you will master debugging your next app and can explore any React-based page or app several times faster compared to a traditional code-check.

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

Further Reading on the Toptal Engineering Blog:

Understanding the basics

  • What is React Developer Tools?

    React Developer Tools is a browser extension for the React JavaScript library. This set of tools can reveal the insides of any React application and monitor the changes without browser logging and debugging.

  • What is the use of React Developer Tools?

    It can show the React component tree, display and update the state/props, track the rendering time, and detect re-render reasons.

  • Is it difficult to set up React Developer Tools?

    No, setting up React Developer tools simply requires installing an extension to your browser.

  • Can I debug Context and useReducer with this tool?

    Yes, you can use React Developer tools to debug Context and useReducer. It supports each part of the React API out of the box.

Freelancer? Find your next job.
React.js Developer Jobs
Teimur Gasanov

Located in Bishkek, Chuy Province, Kyrgyzstan

Member since May 1, 2018

About the author

Teimur is a senior front-end engineer and React expert with wide-ranging web development experience at companies including Klarna and Clubhouse (now Shortcut).

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.