Skip to content
aviral gupta

// B4.4 · ~30 min · Beginner

React Developer Tools

After this lesson you can install React Developer Tools, find a component in its Components tab, read and edit its props and state, and name components so you can find them.

Lesson 4 of 4 in B4 Setting up a project

End of the module

You will be able to

  • Install React Developer Tools as a browser extension, or as the react-devtools package for Safari
  • Use the Components tab to find a component and read or edit its props and state
  • Predict the component tree for a page, and name components so they are easy to find
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B4.3: which rule does the Vite React template's .oxlintrc.json set to error?

  2. Predict · Activity 2 of 7

    Predict before you read on. You open the Components tab for this page. Which tree does it show? It is written here as Parent(children), with a key after #.

    function Header() {
      return <h1>To do</h1>;
    }
    
    function TodoItem({ text }) {
      return <li>{text}</li>;
    }
    
    function TodoList() {
      return (
        <ul>
          <TodoItem key="1" text="Tea" />
          <TodoItem key="2" text="Milk" />
        </ul>
      );
    }
    
    function App() {
      return (
        <main>
          <Header />
          <TodoList />
        </main>
      );
    }
  3. Practice · Activity 3 of 7

    Match each place where your React app runs with the way react.dev gives to inspect it.

  4. Practice · Activity 4 of 7

    Fill in the address of the script tag that connects your page to the standalone react-devtools, as react.dev shows it.

    <script src=""></script>
  5. Practice · Activity 5 of 7

    Both components are arrow functions. Which names does JavaScript give them, as [Cart.name, parts[0].name]?

    const Cart = () => <p>Cart</p>;
    const parts = [() => <p>Part</p>];
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Counter copies its initial prop into state. It renders with initial 0; then its parent renders it again with initial 5, so in DevTools its props now read initial: 5. What does the button show?

    function Counter({ initial }) {
      const [count, setCount] = useState(initial);
      return <button onClick={() => setCount(count + 1)}>Coffee: {count}</button>;
    }
  7. Apply · Activity 7 of 7

    Mini-task: inspect your shopping cart. Install React Developer Tools for your browser (or react-devtools for Safari). Put the shopping cart from lesson B3.6 into src/App.jsx of your Vite project, with data.js next to it, and run npm run dev. In the Components tab, find ShoppingCart, ProductList and Cart. Add Bread twice and read Cart's items prop and ShoppingCart's state. Use the inspect button on a cart line. Then edit a quantity in ShoppingCart's state and watch the page.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

The names the tree will show

Shop renders a Header from its own file, two ProductRow components and a Badge whose file exports an arrow function without a name. The last line prints the name JavaScript gives each component function: these are the names you look for in the Components tab. It runs when main.jsx loads, so it prints before the snapshot of #root.

main.jsx

import Header from './Header.jsx';
import Badge from './Badge.jsx';

function ProductRow({ name }) {
  return <li>{name}</li>;
}

export default function Shop() {
  return (
    <>
      <Header title="Tea shop" />
      <ul>
        <ProductRow name="Green tea" />
        <ProductRow name="Black tea" />
      </ul>
      <Badge count={2} />
    </>
  );
}

// The name JavaScript gives each component function.
console.log(Shop.name, Header.name, ProductRow.name, JSON.stringify(Badge.name));

Header.jsx

export default function Header({ title }) {
  return <h1>{title}</h1>;
}

Badge.jsx

// No name: react.dev discourages this.
export default ({ count }) => <p>{count} items in the cart</p>;

Run it with

npm run dev

Output

Shop Header ProductRow "default"
<!-- #root -->
<div id="root"><h1>Tea shop</h1><ul><li>Green tea</li><li>Black tea</li></ul><p>2 items in the cart</p></div>
  • Badge.jsx exports export default ({ count }) => …: the function has no name of its own, so JavaScript names it default.
  • The tree for this page is Shop, with Header, two ProductRow and the nameless one inside it; the ul and li elements are not in the tree.
  • Fix Badge by naming it: export default function Badge({ count }) { … }. The import name in main.jsx does not rename the function.
  • react.dev: components without names are discouraged because they make debugging harder.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Exercises

Exercise 1 of 2

Components you can find in the tree

Shop does everything inline, so the Components tab shows only Shop. Write two components and export them by name: ProductRow({ product, onAdd }) renders an <li> with the product name, a space and a button "Add " plus the name, which calls onAdd(product); CartSummary({ count }) renders <p>Cart: {count} items</p>. Then render one ProductRow per product, with a key, and CartSummary in Shop.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    Write each one as export function Name(props) { … } above Shop, so it has a name and the tests can import it.

  2. Hint 2

    ProductRow returns the <li> from the map, with {product.name}, {' '} and the button inside.

  3. Hint 3

    In Shop: products.map(product => <ProductRow key={product.id} product={product} onAdd={() => setCount(count + 1)} />), then <CartSummary count={count} />.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';

const products = [
  { id: 'tea', name: 'Tea' },
  { id: 'coffee', name: 'Coffee' }
];

export function ProductRow({ product, onAdd }) {
  return (
    <li>
      {product.name}{' '}
      <button onClick={() => onAdd(product)}>Add {product.name}</button>
    </li>
  );
}

export function CartSummary({ count }) {
  return <p>Cart: {count} items</p>;
}

export default function Shop() {
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>Shop</h1>
      <ul>
        {products.map(product => (
          <ProductRow key={product.id} product={product} onAdd={() => setCount(count + 1)} />
        ))}
      </ul>
      <CartSummary count={count} />
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { useState } from 'react';

const products = [
  { id: 'tea', name: 'Tea' },
  { id: 'coffee', name: 'Coffee' }
];

// TODO: write ProductRow({ product, onAdd }) and CartSummary({ count }),
// export them, and render them below instead of the inline markup.

export default function Shop() {
  const [count, setCount] = useState(0);

  return (
    <>
      <h1>Shop</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name}{' '}
            <button onClick={() => setCount(count + 1)}>Add {product.name}</button>
          </li>
        ))}
      </ul>
      <p>Cart: {count} items</p>
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Shop, * as parts from './main.jsx';

test('ProductRow shows its product and passes it to onAdd', async () => {
  const user = userEvent.setup();
  const {ProductRow} = parts;
  assert.equal(typeof ProductRow, 'function', 'main.jsx exports no function ProductRow');
  const added = [];
  render(<ul><ProductRow product={{ id: 'tea', name: 'Tea' }} onAdd={(product) => added.push(product)} /></ul>);
  assert.equal(screen.getByRole('listitem').textContent, 'Tea Add Tea', 'the row reads ' + JSON.stringify(screen.getByRole('listitem').textContent));
  await user.click(screen.getByRole('button', {name: 'Add Tea'}));
  assert.equal(added[0]?.name, 'Tea', 'onAdd received ' + JSON.stringify(added));
});

test('CartSummary shows the count it receives', () => {
  const {CartSummary} = parts;
  assert.equal(typeof CartSummary, 'function', 'main.jsx exports no function CartSummary');
  render(<CartSummary count={2} />);
  assert.equal(document.body.textContent, 'Cart: 2 items', 'CartSummary with count 2 reads ' + JSON.stringify(document.body.textContent));
});

test('The shop counts what you add', async () => {
  const user = userEvent.setup();
  render(<Shop />);
  await user.click(screen.getByRole('button', {name: 'Add Tea'}));
  await user.click(screen.getByRole('button', {name: 'Add Coffee'}));
  assert.equal(screen.queryByText(/^Cart:/)?.textContent, 'Cart: 2 items', 'after two adds the page reads ' + JSON.stringify(document.body.textContent));
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Exercise 2 of 2

Follow the wrong prop to its parent

The cart holds 2 Tea and 1 Coffee, but the summary says Cart: 2 items. In the Components tab, CartSummary shows props count: 2, and it is rendered by Cart. CartSummary displays what it gets, so the bug is in the parent that passes count. Fix it in Cart: count the quantities, so the summary shows 3, and 4 after Add Tea.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads React and the compiler that turns JSX into JavaScript (up to 2 MB) and keeps them cached. Your components run in your browser and stay on your device.

Your page

Your page runs in an isolated frame. It has no access to this site, and its localStorage is kept in memory for this run only: the next run starts empty.

Run the program to see its page here.

Hints
  1. Hint 1

    items.length counts the lines, not the cups: 2 lines, 3 cups.

  2. Hint 2

    Add up item.quantity over items, while rendering: items.reduce((sum, item) => sum + item.quantity, 0).

  3. Hint 3

    Store the result in const count and pass count={count} to CartSummary. Do not put it in state.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import { useState } from 'react';

function CartLine({ item }) {
  return <li>{item.name} × {item.quantity}</li>;
}

function CartSummary({ count }) {
  return <p>Cart: {count} items</p>;
}

export default function Cart() {
  const [items, setItems] = useState([
    { id: 'tea', name: 'Tea', quantity: 2 },
    { id: 'coffee', name: 'Coffee', quantity: 1 }
  ]);

  function addTea() {
    setItems(items.map(item =>
      item.id === 'tea' ? { ...item, quantity: item.quantity + 1 } : item
    ));
  }

  const count = items.reduce((sum, item) => sum + item.quantity, 0);

  return (
    <>
      <ul>
        {items.map(item => <CartLine key={item.id} item={item} />)}
      </ul>
      <CartSummary count={count} />
      <button onClick={addTea}>Add Tea</button>
    </>
  );
}
Run it on your computer

Install React 19.3 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.jsx

import { useState } from 'react';

function CartLine({ item }) {
  return <li>{item.name} × {item.quantity}</li>;
}

function CartSummary({ count }) {
  return <p>Cart: {count} items</p>;
}

export default function Cart() {
  const [items, setItems] = useState([
    { id: 'tea', name: 'Tea', quantity: 2 },
    { id: 'coffee', name: 'Coffee', quantity: 1 }
  ]);

  function addTea() {
    setItems(items.map(item =>
      item.id === 'tea' ? { ...item, quantity: item.quantity + 1 } : item
    ));
  }

  return (
    <>
      <ul>
        {items.map(item => <CartLine key={item.id} item={item} />)}
      </ul>
      {/* DevTools shows CartSummary with props count: 2. */}
      <CartSummary count={items.length} />
      <button onClick={addTea}>Add Tea</button>
    </>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Cart from './main.jsx';

test('The summary counts every item: 2 Tea and 1 Coffee are 3', () => {
  render(<Cart />);
  assert.equal(screen.queryByText(/^Cart:/)?.textContent, 'Cart: 3 items', 'the summary reads ' + JSON.stringify(screen.queryByText(/^Cart:/)?.textContent));
});

test('Add Tea adds one to the line and to the summary', async () => {
  const user = userEvent.setup();
  render(<Cart />);
  await user.click(screen.getByRole('button', {name: 'Add Tea'}));
  assert.equal(screen.queryByText(/^Tea/)?.textContent, 'Tea × 3', 'the Tea line reads ' + JSON.stringify(screen.queryByText(/^Tea/)?.textContent));
  assert.equal(screen.queryByText(/^Cart:/)?.textContent, 'Cart: 4 items', 'the summary reads ' + JSON.stringify(screen.queryByText(/^Cart:/)?.textContent));
});

In a Vite React project (npm create vite@latest, template react), put main.jsx in src/ as App.jsx and start it with npm run dev. React renders it inside <StrictMode>, as here.

Run the program:

npm run dev

There is no command for the checks on your computer yet. They are in main.test.jsx, written with node:test and React Testing Library.

Common mistakes

Setting the parent's state while rendering

import { useState } from 'react';

function CartBadge({ items, onCount }) {
  onCount(items.length); // tells the parent while rendering
  return <span>{items.length} items</span>;
}

export default function Shop() {
  const [count, setCount] = useState(0);
  const items = ['Tea', 'Coffee'];
  return (
    <>
      <h1>Shop ({count})</h1>
      <CartBadge items={items} onCount={setCount} />
    </>
  );
}

What React prints

Cannot update a component (`Shop`) while rendering a different component (`CartBadge`).

Why, and the fix

CartBadge sets Shop's state while it renders, which React does not allow. The warning names both components, as the Components tab does, so you know where to look. Shop has the items already: compute const count = items.length; in Shop and drop onCount. With an unnamed component, the warning would say Unknown instead.

Importing a whole module as a component

import * as CartSummary from './CartSummary.jsx';

export default function Shop() {
  return <CartSummary count={2} />;
}

What React prints

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

Why, and the fix

import * as CartSummary gives you the module object, with the component in its default property, so <CartSummary /> asks React to render an object. Import the default export itself: import CartSummary from './CartSummary.jsx';.

A nameless component in a warning

import ProductList from './ProductList.jsx';

export default function Shop() {
  return <ProductList names={['Tea', 'Coffee']} />;
}

What React prints

Check the render method of `default`.

Why, and the fix

The list has no keys, so React warns, and it names the component whose render returned the list: default, the name JavaScript gave the anonymous arrow. With several such files, that does not tell you which. Name the function, export default function ProductList({ names }) { … }, and give each <li> a key: <li key={name}>.

React in the browser: React 19.3.0 (development build) and React Testing Library, MIT, served from this site; JSX is compiled by TypeScript 6.0.3, Apache-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Install it

React Developer Tools let you inspect React components, edit props and state, and find performance problems. The easiest way is the browser extension, which react.dev lists for Chrome, Firefox and Edge. On a site built with React you then see two new panels in the browser's developer tools: Components and Profiler. For other browsers, such as Safari, install the npm package globally, npm install -g react-devtools, start it with react-devtools in a terminal, add <script src="http://localhost:8097"></script> at the beginning of your page's <head>, and reload. React Native apps use React Native DevTools.

The Components tab

The Components tab shows your components as a tree: on react.dev's screenshot, App contains List, which contains three ListItem components with key="1" to key="3". The <ul> and <li> elements they return are not in it; the Elements tab has those. Select a component to see its props, its hooks (where state lives) and the components that rendered it. To find the component behind something on the page, click the inspect button in the top left corner of the tab, then click the element. You can edit props and state there and watch the page update. With Strict Mode, logs from the second render appear dimmed.

Names make components findable

The tree shows each component by its name, and a JavaScript function's name is what it is called in your code: function Cart() is named Cart, and const Cart = () => … is named Cart too. An arrow with no name to take, such as export default () => …, is named default, and React's own warnings call a nameless component Unknown. react.dev discourages components without names because they make debugging harder: always give meaningful names to your component functions and their files. A component you call as a function, Row(), instead of rendering it as <Row />, is not in the tree at all.

Sources

Last reviewed October 5, 2026