Debug a React Native Contact Directory
Hunt down and fix bugs in a contact app using React Native DevTools.
Introduction
30 Second Summary
Every time an app on your phone shows a blank screen or ignores your taps, there is a developer somewhere trying to figure out why. The tools they reach for to track down those invisible problems are debugging tools.
In this project, you will build a React Native contact directory app with bugs baked right in. Then you will use React Native DevTools to hunt down and fix each bug, one panel at a time.
What You'll Build
Picture scrolling through a polished contact list, tapping any name to see their full profile, and knowing that every bug between you and that smooth experience was one you personally hunted down.
By the end of this project, you'll have:
- A scrollable contact list with profile photos, names, and emails pulled from a live API.
- A detail view that shows a contact's full profile when you tap their card.
- A performance-optimized list that only re-renders the cards that actually changed, verified with the React Profiler.
- Secret Mission: Add a search bar to filter contacts by name, and debug it using conditional breakpoints and logpoints.
Are there any prerequisites?
This project assumes basic familiarity with React Native components like View, Text, and FlatList. All tools and debugging concepts are covered step by step.
Before We Start
Before we dive in, let's get clear on what you're about to build and why it matters.
Set Up Your Expo Project
You need a running React Native app before you can debug one. Expo gives you the fastest path from zero to a working app in the iOS Simulator.
In this step, you'll create a new Expo project and launch it in the Simulator so you have a live app ready for the bugs ahead.
In this step, get ready to:
- Create a new Expo project using the blank template with SDK 57.
- Launch the app in the iOS Simulator.
Create a new Expo project
- Move to your Desktop by running this command:
cd ~/Desktop
- Create a new Expo project by running this command:
npx create-expo-app@latest --template blank@sdk-57
- If npx asks to install the package, enter y to confirm.
- When prompted for a project name, enter contact-directory.
- Move into your new project folder by running:
cd contact-directory
- Confirm the project files were created by running:
ls
You should see App.js, package.json, node_modules, and other project files listed in the output.
Why the blank template?
The default Expo template includes tab navigation, routing, and TypeScript. The blank template gives you a single App.js file with no extras.
This keeps you focused on learning React Native DevTools instead of untangling a complex starter project.
Not seeing project files?
- Make sure you ran the npx create-expo-app@latest command from ~/Desktop. Run pwd to check your current directory.
- If the command failed with a network error, check your internet connection and try again.
- Why did my Expo project creation fail?
Start the development server
- Start the Expo development server by running:
npx expo start
What does this command do?
npx expo start launches Metro, the JavaScript bundler for React Native. Your terminal now shows a menu of keyboard shortcuts for interacting with the dev server.
Keep this terminal running for the rest of the project. If you close it or press Ctrl+C, the dev server stops and the app in the Simulator disconnects.
- Press i in the terminal to launch the iOS Simulator.
The first build may take a minute or two. While the app loads, what do you think the default blank template will show on screen?
The Simulator displays a white screen with the text "Open up App.js to start working on your app!" This confirms your Expo project is set up and running correctly.
Simulator not launching?
- Make sure you have opened Xcode at least once to accept the license agreement. The Simulator relies on Xcode's developer tools.
- If no Simulator appears after pressing i, open Xcode, go to Settings, then Platforms, and confirm an iOS Simulator runtime is downloaded.
- Why won't the iOS Simulator launch when I press i in Expo?
Your app is up and running in the Simulator. Next up, you'll fill it with contact directory code, complete with a couple of hidden bugs waiting to be found.
Build a Broken Contact List
Your Expo project is running in the iOS Simulator with the default template screen. Before you can debug anything, you need an app with something to debug.
In this step, you'll replace the template code in App.js with a full contact directory that fetches 50 random users from an API and displays them in a scrollable list. Two bugs are baked into the code on purpose. You won't fix them here. You'll just see the app break and wonder why.
In this step, get ready to:
- Replace App.js with the contact directory components, fetch logic, and styles.
- See the app reload with a broken contact list caused by two hidden bugs.
Clear App.js and add the component code
The contact directory has three main pieces: a ContactCard component for each list item, a ContactDetail component for the full-info view, and the main App function that ties everything together. You'll add each piece one at a time.
- In VS Code, open App.js.
- Select all the existing code by pressing Cmd+A (macOS) or Ctrl+A (Windows), then delete it.
- Paste the following imports and ContactCard component:
import { useState, useEffect } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
Image,
TouchableOpacity,
ActivityIndicator,
SafeAreaView,
} from 'react-native';
// Each contact card shows a photo, name, and email
function ContactCard({ contact, onPress }) {
return (
<TouchableOpacity style={styles.card} onPress={() => onPress(contact)}>
<Image
source={{ uri: contact.picture.thumbnail }}
style={styles.thumbnail}
/>
<View style={styles.cardText}>
<Text style={styles.name}>
{contact.name.first} {contact.name.last}
</Text>
<Text style={styles.email}>{contact.email}</Text>
</View>
</TouchableOpacity>
);
}
What does ContactCard do?
ContactCard is a function component that renders one row in the contact list. It shows a thumbnail photo, the person's full name, and their email address.
The onPress prop fires when the learner taps a card, passing the contact object up to the parent component.
- Add the ContactDetail component below ContactCard by pasting this code:
// Detail view shows full info for a selected contact
function ContactDetail({ contact, onBack }) {
return (
<View style={styles.detailContainer}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Text style={styles.backText}>Back</Text>
</TouchableOpacity>
<Image
source={{ uri: contact.picture.large }}
style={styles.detailImage}
/>
<Text style={styles.detailName}>
{contact.name.first} {contact.name.last}
</Text>
<Text style={styles.detailInfo}>{contact.email}</Text>
<Text style={styles.detailInfo}>{contact.phone}</Text>
<Text style={styles.detailInfo}>
{contact.location.city}, {contact.location.country}
</Text>
</View>
);
}
What does ContactDetail do?
ContactDetail renders a full-screen view for a single contact. It displays a larger photo along with the person's name, email, phone number, and location.
The onBack prop lets the user return to the main list.
- Save App.js.
The app tries to reload but shows a red error screen. This is expected because the file does not have a default export yet.
Add the App function
The App function is the heart of the app. It manages state, fetches data from the randomuser.me API, and decides whether to show the list or the detail view.
- Add the App function's state and data-fetching logic below ContactDetail by pasting this code:
export default function App() {
// State for the contact list, loading spinner, and selected contact
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedContact, setSelectedContact] = useState(null);
// Fetch contacts when the app first loads
useEffect(() => {
fetchContacts();
}, []);
async function fetchContacts() {
try {
const response = await fetch(
'https://randomuser.me/apii/?results=50'
);
const data = await response.json();
setContacts(data.results);
} catch (error) {
console.error('Failed to fetch contacts:', error);
} finally {
setLoading(false);
}
}
// Look up the selected contact by UUID
function handleSelectContact(uuid) {
const found = contacts.find(c => c.id.value === uuid);
setSelectedContact(found);
}
What does this code do?
Three state variables track the contact list, a loading flag, and which contact is currently selected.
useEffect calls fetchContacts when the app first mounts. That function hits the randomuser.me API and stores the results. handleSelectContact looks up a contact by UUID and sets it as the selected contact.
- Add the render logic directly below handleSelectContact by pasting this code:
// If a contact is selected, show the detail view
if (selectedContact) {
return (
<SafeAreaView style={styles.container}>
<ContactDetail
contact={selectedContact}
onBack={() => setSelectedContact(null)}
/>
</SafeAreaView>
);
}
// Otherwise, show the scrollable contact list
return (
<SafeAreaView style={styles.container}>
<Text style={styles.header}>Contacts</Text>
{loading ? (
<ActivityIndicator size="large" color="#007AFF" />
) : (
<FlatList
data={contacts}
keyExtractor={(item) => item.login.uuid}
renderItem={({ item }) => (
<ContactCard
contact={item}
onPress={(contact) => handleSelectContact(item.login.uuid)}
/>
)}
/>
)}
</SafeAreaView>
);
}
What does the render do?
If a contact is selected, the app shows ContactDetail. Otherwise, it shows the main list.
While loading, an ActivityIndicator spinner appears. Once loading finishes, a FlatList renders one ContactCard per contact.
- Save App.js.
The error changes. Now the app crashes because styles is referenced throughout the components but hasn't been defined yet. You'll add that next.
Add the styles and test the app
The last piece is a StyleSheet that controls the layout and appearance of every component. You'll paste it in three parts because of its length.
- Add the first group of styles below the closing brace of the App function by pasting this code:
const styles = StyleSheet.create({
// Main app background
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
// Header bar at the top
header: {
fontSize: 28,
fontWeight: 'bold',
padding: 20,
backgroundColor: '#fff',
},
// Each contact card row
card: {
flexDirection: 'row',
padding: 12,
marginHorizontal: 16,
marginVertical: 4,
backgroundColor: '#fff',
borderRadius: 8,
alignItems: 'center',
},
- Continue adding styles directly below by pasting this code:
// Circular profile photo
thumbnail: {
width: 50,
height: 50,
borderRadius: 25,
},
// Text area next to the thumbnail
cardText: {
marginLeft: 12,
flex: 1,
},
// Contact name
name: {
fontSize: 16,
fontWeight: '600',
},
// Email below the name
email: {
fontSize: 13,
color: '#666',
marginTop: 2,
},
- Add the final group of styles to close out the stylesheet by pasting this code:
// Detail view layout
detailContainer: {
flex: 1,
alignItems: 'center',
padding: 20,
},
backButton: {
alignSelf: 'flex-start',
marginBottom: 20,
},
backText: {
fontSize: 16,
color: '#007AFF',
},
// Large photo on detail screen
detailImage: {
width: 120,
height: 120,
borderRadius: 60,
marginBottom: 16,
},
detailName: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
detailInfo: {
fontSize: 16,
color: '#444',
marginBottom: 4,
},
});
What do these styles do?
The StyleSheet.create block defines the visual appearance of every component: the app background, the header bar, the card rows with circular thumbnails, and the full-screen detail view.
In React Native, styles work like CSS but use JavaScript objects. Properties are camelCase instead of kebab-case.
- Save App.js.
The red error screen clears and the app reloads via Fast Refresh.
Before you look at the Simulator, make a prediction: do you think the contact list loaded successfully with 50 names?
The app shows a "Contacts" header and a loading spinner that stops, leaving a completely empty list. Tapping anywhere does nothing. Two bugs are hiding in the code, and you have no visible clue about what went wrong.
Seeing a red error screen instead of an empty list?
- Make sure the closing brace and parenthesis of each component match. A missing ) or } causes a syntax error.
- Check that the StyleSheet.create({ block ends with }); at the very end of the file.
- Confirm you pasted all three style groups in order. Missing any group means some style references are undefined.
I'm seeing a red error screen after pasting the contact directory code into App.js. Help me figure out what's wrong.
✔️ Awesome, I've got everything!
Great. Double check you've saved App.js before moving on.
ⓧ I'd like to double check the full code
Your App.js should contain these sections in order. Scroll through your file and confirm each one is present.
- Imports from react and react-native (lines 1-11).
- ContactCard component that renders a card with a thumbnail, name, and email.
- ContactDetail component that renders a full-screen view with a large photo, name, email, phone, and location.
- App function with three state variables, a fetchContacts function, a handleSelectContact function, and the render logic returning a FlatList.
- StyleSheet.create block with 13 style definitions: container, header, card, thumbnail, cardText, name, email, detailContainer, backButton, backText, detailImage, detailName, and detailInfo.
Your app is running but clearly broken. Something is preventing the contacts from loading, and tapping does nothing. In the next step, you'll open React Native DevTools and track down the first bug.
Fix the Network Failure with DevTools
Your contact directory code is in place, but the app is stuck on an empty list.
There is no error on screen and no crash. Without a debugging tool, you would be left guessing what went wrong. In this step, you will open React Native DevTools and use the Network panel to see exactly what HTTP request the app made and what response came back.
In this step, get ready to:
- Open React Native DevTools and inspect the Console panel for errors.
- Use the Network panel to identify the failed API request.
- Fix the broken URL and confirm contacts load.
Open React Native DevTools
The app shows an empty list, but there is no visible error telling you what went wrong. Your first move is to open the debugging tools and look for clues.
- Switch to the terminal window where your development server is running.
- Press j to open React Native DevTools.
A new DevTools window opens in your browser.
What is React Native DevTools?
React Native DevTools is the official debugging tool built into React Native. It gives you panels for inspecting console output, network requests, component trees, and performance profiles.
It replaces the older Flipper debugger and works with the Hermes engine that Expo uses by default.
- In the DevTools window, click the Console tab at the top.
You should see an error message that starts with Failed to fetch contacts: followed by an error object. This is the console.error call from the catch block in your fetchContacts function. It confirms the fetch failed, but it does not tell you why.
DevTools didn't open?
React Native DevTools requires Google Chrome or Microsoft Edge to be installed on your machine. Make sure one of them is installed and set as your default browser.
If pressing j does nothing, make sure you are pressing it in the terminal where the Expo dev server is running, not in a different terminal window.
Help me open React Native DevTools
Inspect the failed request in the Network panel
The Console told you the fetch failed. The Network panel will show you exactly what URL was requested and what response the server sent back.
- Click the Network tab in the DevTools window.
You should see a request to https://randomuser.me/apii/?results=50 with a 404 status. The 404 means the server could not find the page at that URL path.
- Click on the failed request row to inspect its details.
- Look at the full URL in the request details. Notice the path says /apii/ instead of /api/. There is an extra i in the path.
What does the Network panel show you?
The Network panel logs every HTTP request your app makes. For each request, you can see the URL, the HTTP status code, timing information, headers, and the full response body.
In this case, the 404 status and the URL path immediately reveal the problem. The Console error alone would have left you guessing whether the issue was a network failure, a server outage, or a bad URL.
Fix the URL typo
The Network panel told you exactly what is wrong. The fetch URL has a typo. Time to fix it.
- In App.js, scroll down to the fetchContacts function.
- Find this line inside the try block:
'https://randomuser.me/apii/?results=50'
- Change apii to api so the line now reads:
'https://randomuser.me/api/?results=50'
Before you save, what do you expect to see when the app reloads with the corrected URL?
- Save App.js.
The app reloads via Fast Refresh. The contact list populates with 50 cards, each showing a profile photo, a name, and an email address.
- Switch back to the DevTools window and click the Network tab. Confirm you see a new request to https://randomuser.me/api/?results=50 with a 200 status.
- Try tapping one of the contact cards in the Simulator.
Nothing happens. The detail view does not open. That is the second bug, and you will hunt it down in the next step.
Still seeing an empty list?
Double check the URL string in your code. Make sure it reads /api/ with exactly one i, not /apii/ or /ap/.
Make sure you saved the file. Check the Network panel again for a fresh request. If the request still shows a 404, the URL is still wrong.
Help me fix my contact list fetch
✔️ Awesome, I've got everything!
Great. Make sure you have saved App.js and that your contact list shows 50 cards in the Simulator.
ⓧ I'd like to double check the full code
The only change you made in this step was inside the fetchContacts function. Scroll to that function and confirm the fetch URL reads exactly:
const response = await fetch(
'https://randomuser.me/api/?results=50'
);
Everything else in the file is unchanged from the previous step.
One bug down, one to go. Your contacts are loading, but tapping a card does nothing. Next up, you will use Console logging and breakpoints to trace the data flow and find exactly where the second bug breaks things.
Hunt Down the State Bug
In the last step, you used the Network panel to fix a broken API URL and bring 50 contacts to life on screen. There is still a problem though. Tapping any contact card does absolutely nothing.
The code looks reasonable at a glance. The onPress handler calls a function, that function searches the contacts array, and the result should open the detail view. To find where the data flow breaks, you need to trace the actual runtime values. This is where Console logging and breakpoints come in.
In this step, get ready to:
- Add a console.log to trace the contact lookup.
- Set a breakpoint to inspect why contacts.find() returns undefined.
- Fix the bug and verify the detail view works.
Add a console.log to trace the contact lookup
The handleSelectContact function runs when you tap a contact card, but you cannot see what values it is working with. Adding a console.log reveals exactly what flows through the function at runtime.
- In App.js, find the handleSelectContact function. It currently looks like this:
function handleSelectContact(uuid) {
const found = contacts.find(c => c.id.value === uuid);
setSelectedContact(found);
}
- Add a console.log line between the const found line and the setSelectedContact line, so the function looks like this:
function handleSelectContact(uuid) {
const found = contacts.find(c => c.id.value === uuid);
console.log('uuid:', uuid, 'found:', found);
setSelectedContact(found);
}
What does this console.log do?
This line prints two values every time you tap a contact: the uuid being searched for and the result of contacts.find(). If found comes back as undefined, you know the search is failing.
- Save App.js.
- Switch back to React Native DevTools and open the Console panel.
Before you tap a contact, what do you expect to see in the Console? Will found be a full contact object, or something else?
- Tap any contact card in the iOS Simulator.
In the Console panel, you see the output. The uuid value is logged as a UUID string, but found is undefined. The function runs, but the search comes up empty every time.
Not seeing any console output?
- Make sure you saved App.js after adding the console.log line. Fast Refresh only picks up saved changes.
- Check that the console.log is inside the handleSelectContact function body, not outside it.
- Why isn't my console.log showing up after tapping a contact card?
Set a breakpoint to reveal the mismatch
The Console told you that found is undefined, but not why. A breakpoint pauses the app mid-execution so you can inspect every variable in scope. You need to compare the value contacts.find() is checking against the value it is searching for.
- In DevTools, switch to the Sources panel.
- Press Cmd+P (macOS) or Ctrl+P (Windows) to open the file finder.
- Type App.js and select it from the results.
- Find the line that reads const found = contacts.find(c => c.id.value === uuid);.
- Click the line number in the gutter to the left of that line to set a breakpoint. A blue marker appears on the line number.
Before you tap a contact, think about what c.id.value and uuid might contain. Do you think they hold the same kind of value?
- Tap any contact card in the iOS Simulator.
The app pauses and a Paused in Debugger overlay appears on the Simulator screen. In DevTools, the Sources panel highlights the breakpoint line.
- Look at the Scope panel on the right side of the Sources panel.
- Find the uuid parameter. It holds a UUID string like 7a0eed16-9430-4d68-901f-c0d4c1c3bf00.
- Expand a contact object in the contacts array and look at its id.value field. It holds a national identifier like 405-88-3636 (an SSN-format string).
Why does contacts.find() always return undefined?
The code compares c.id.value against uuid, but these two fields hold completely different kinds of data. id.value is a national ID (like an SSN), while login.uuid is a UUID string.
They will never match, so find() always returns undefined. Then setSelectedContact(undefined) is called, which is falsy, so the if (selectedContact) check never passes and the detail view never renders.
- Click the blue play button (or press F8) in the Sources panel to resume execution.
- Click the breakpoint marker on the line number to remove it. You are done inspecting.
Fix the bug by passing the contact directly
Now that you know the lookup is comparing mismatched fields, the cleanest fix is to remove the lookup entirely. The ContactCard component already has the full contact object. You can pass it straight through to setSelectedContact instead of searching for it by ID.
- In App.js, find the handleSelectContact function (including the console.log you added earlier):
function handleSelectContact(uuid) {
const found = contacts.find(c => c.id.value === uuid);
console.log('uuid:', uuid, 'found:', found);
setSelectedContact(found);
}
- Delete the entire handleSelectContact function, including all four lines shown above.
- Now scroll down to the FlatList and find the onPress prop inside renderItem. It currently looks like this:
onPress={(contact) => handleSelectContact(item.login.uuid)}
- Replace that line so it passes the contact object directly to setSelectedContact:
onPress={(contact) => setSelectedContact(contact)}
Why does this fix work?
When the learner taps a card, ContactCard already calls onPress(contact) with the full contact object. The old code ignored that object and ran a broken search instead.
The new onPress callback receives that same contact object and hands it straight to setSelectedContact. No lookup needed.
- Save App.js. The app reloads via Fast Refresh.
Do you think tapping a contact will open the detail view now, or will something else go wrong?
- Tap any contact card in the iOS Simulator.
The detail view appears, showing the correct person's photo, name, email, phone number, and location.
- Tap Back to return to the contact list.
- In DevTools, switch to the Components panel.
- Tap a contact card again so the detail view opens.
- In the Components panel, select the ContactDetail component in the tree. On the right, confirm the correct contact object is shown in its props.
Detail view not appearing when you tap?
- Make sure you deleted the entire handleSelectContact function. If it still exists, the old onPress reference might still call it.
- Check that the onPress prop reads onPress={(contact) => setSelectedContact(contact)}. A typo in the parameter name or the function call will break it silently.
- Why isn't the contact detail view opening after I changed the onPress handler?
✔️ Awesome, I've got everything!
Great. Make sure you have saved App.js and that the detail view opens when you tap any contact.
ⓧ I'd like to double check the full code
Scroll up through your App.js and confirm these two changes are in place:
- The handleSelectContact function is completely removed (no trace of it anywhere in the file).
- Inside the FlatList renderItem, the onPress prop reads onPress={(contact) => setSelectedContact(contact)}.
- The temporary console.log line is gone (it was inside the deleted function).
Both bugs are squashed. Your contact list loads from the API and tapping any card opens the correct detail view. Next up, you will use the React Profiler to find a hidden performance problem lurking in those 50 contact cards.
Profile and Fix Re-renders
Your contact directory is fully functional. Tapping any card opens the right person's details, and the Back button returns you to the list.
There's one more issue hiding under the surface. Every time state changes, all 50 ContactCard components re-render, even the ones whose data has not changed. The React Profiler will show you exactly where the wasted renders are happening.
In this step, get ready to:
- Spot unnecessary re-renders using the Components panel highlight mode.
- Profile component render times with the React Profiler.
- Optimize ContactCard with React.memo to skip unnecessary re-renders.
Spot re-renders with highlight mode
React Native DevTools can visually highlight every component that re-renders. This turns an invisible performance problem into something you can see with your own eyes.
- In React Native DevTools, switch to the Components panel.
- Click the gear icon (View Settings) in the Components panel toolbar.
- Check the box labelled Highlight updates when components render.
- In the Simulator, tap any contact card to open the detail view.
- Tap Back to return to the contact list.
Every single ContactCard component flashes. All 50 cards re-rendered even though none of their data changed.
Why do all 50 cards re-render?
When you tap a contact, setSelectedContact updates the App component's state. By default, React re-renders the parent and every child inside it, even if that child's props are identical.
With 50 cards, that is a lot of wasted work. The more cards in the list, the worse the problem gets.
Profile the re-renders
The highlight overlay shows you that re-renders are happening. The Profiler shows you exactly how many components rendered and how long each one took.
- Switch to the Profiler panel in DevTools.
- Click the record button (circle icon) to start recording.
- In the Simulator, tap a contact card to open the detail view.
- Tap Back to return to the list.
- Click the record button again to stop recording.
The flame chart appears. Every ContactCard component shows up as a bar, meaning all 50 rendered during that interaction.
What does the flame chart tell you?
Each bar in the flame chart represents a component that rendered during the recorded interaction. The width of the bar shows how long that render took.
Right now, every single ContactCard has a bar, even though only the selected contact changed. That is 49 wasted renders per interaction.
Wrap ContactCard with React.memo
React.memo is a wrapper that tells React to skip re-rendering a component if its props have not changed. Wrapping ContactCard with it means each card only re-renders when its own contact data or callback actually changes.
- In App.js, find the first import line at the top of the file:
import { useState, useEffect } from 'react';
- Add memo to the import so the line looks like this:
import { useState, useEffect, memo } from 'react';
- Now find the ContactCard function declaration a few lines below the imports:
function ContactCard({ contact, onPress }) {
- Replace that line with this, wrapping the function inside memo():
const ContactCard = memo(function ContactCard({ contact, onPress }) {
- Scroll down to the closing brace of the ContactCard component (right before the ContactDetail function). Find this closing:
);
}
- Change the closing } to }); to close the memo() call:
);
});
- Save App.js. The app reloads via Fast Refresh in the Simulator.
What does React.memo do?
memo() wraps a component and tells React: before re-rendering this, compare the new props to the old props. If they are the same, skip the render entirely.
Each ContactCard receives the same contact object and the same onPress callback on each render. With memo, React sees the props are identical and skips re-rendering those 49 unchanged cards.
Seeing an error after saving?
- Check that you added memo to the import on line 1. Without the import, memo is undefined and the app crashes.
- Make sure the closing of ContactCard is }); and not just }. The extra ) closes the memo() call.
Help me fix a React.memo syntax error in my ContactCard component.
Before you record a new Profiler session, what do you expect the flame chart to look like now compared to your first recording?
- Switch back to the Profiler panel in DevTools.
- Click the record button to start a new recording.
- In the Simulator, scroll through the contact list, then tap a contact and tap Back.
- Click the record button again to stop.
The flame chart now shows far fewer ContactCard renders. Only the components whose props actually changed re-rendered.
- Switch to the Components panel and confirm Highlight updates when components render is still checked.
- Tap a contact and go back. Only the components that actually changed flash now, not all 50 cards.
✔️ Awesome, I've got everything!
Great. Double check you have saved App.js.
ⓧ I'd like to double check the full code
Scroll through your App.js file and confirm these three changes are in place, in order:
- Line 1: the import reads import { useState, useEffect, memo } from 'react';.
- The ContactCard declaration reads const ContactCard = memo(function ContactCard({ contact, onPress }) {.
- The closing brace of ContactCard is }); (not just }).
- Everything else in the file is unchanged from the previous step.
Secret mission
Add Search with Conditional Breakpoints
Your contact directory works, but scrolling through 50 contacts to find one person is slow. Add a search bar that filters contacts by name. Then debug it using conditional breakpoints and logpoints, two advanced techniques that let you inspect runtime values without pausing the app on every keystroke or adding console.log statements to your code.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Expo development server (running in your terminal).
- React Native DevTools (browser window).
- iOS Simulator.
- The contact-directory project folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building or want to continue experimenting with the debugging tools.
- Your Expo dev server stays running in the terminal.
- React Native DevTools and the iOS Simulator stay open.
- Your project folder remains on your Desktop.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep your project files so you can pick up where you left off.
- In the terminal where the Expo dev server is running, press Ctrl+C to stop the server.
- Close the React Native DevTools browser window.
- Quit the iOS Simulator by pressing Cmd+Q.
- Your contact-directory folder stays on your Desktop. To resume, open a terminal, cd into the folder, and run npx expo start.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- In the terminal where the Expo dev server is running, press Ctrl+C to stop the server.
- Close the React Native DevTools browser window.
- Quit the iOS Simulator by pressing Cmd+Q.
- Delete the project folder by running this command in your terminal:
rm -rf ~/Desktop/contact-directory
Nice Work!
Nice Work!
Nice work! You just built a React Native contact directory app, broke it on purpose, and fixed every bug using professional debugging tools.
You've learned how to:
- Built a contact directory app that fetches 50 contacts from a live API and displays them in a scrollable list with photos, names, emails, and tap-to-view detail screens.
- Diagnosed and fixed bugs using three different React Native DevTools panels. The Network panel revealed a 404 from a URL typo. Console logging and breakpoints in the Sources panel exposed a silent ID mismatch that prevented the detail view from rendering.
- Identified unnecessary re-renders using the Components Inspector highlight mode and the React Profiler flame chart, then fixed the performance issue with React.memo.
- Secret Mission: Added a search bar with name filtering and debugged it using conditional breakpoints and logpoints for surgical, code-free runtime inspection.
Ready to quiz yourself?