React Native Advanced: Performance, Native Code and Releasing
Testing and Debugging
In this lesson you will
- Debug with React Native DevTools and the developer menu
- Catch screen crashes with an error boundary and a retry button
- Write a component test with Jest and React Native Testing Library
Introduction
Every app has bugs. The difference between a hobby project and a reliable app is how fast you find them, how gracefully the app fails, and whether tests catch the problem before your users do. This lesson gives you tools for all three.
Debugging while you build
React Native DevTools is the debugger Expo recommends. Its docs say: "For the most stable debugging experience, use the React Native DevTools." Start your app, then press j in the terminal where Expo is running. A window opens with panels:
- Console: see
console.logoutput, run JavaScript, inspect objects. - Sources: read your source files and set breakpoints, which pause the app on a line so you can look at the variables.
- Network: every request your app makes, with its status and response.
- React Components: the component tree, with props and state you can edit live.
- React Profiler and Performance: where rendering time goes, as the last lesson used.
The developer menu is built into Expo Go and development builds. Open it by pressing m in the terminal, by shaking a real phone, or with the keyboard shortcut for your emulator. It lets you reload the app, open DevTools and toggle Fast Refresh.
Habits that save hours:
- Read the error message first, and the first line of the stack trace. The red error screen names the file and line.
- Reproduce before you fix. Find exact steps that trigger the bug.
- Shrink the problem. Remove code until the bug disappears, then add back the last thing.
- Log what you know, not what you guess.
console.log({ user, items })beats a hunch.
Quick quiz
How do you open React Native DevTools in an Expo project?
When a screen crashes
An error thrown while a screen renders would normally take down the whole app. An error boundary catches the error and shows a fallback instead, so one broken screen does not close everything.
Expo Router lets any route file export a function named ErrorBoundary. It receives the error and a retry function:
React (TSX)
import { ErrorBoundaryProps } from "expo-router";
import { Pressable, StyleSheet, Text, View } from "react-native";
export function ErrorBoundary({ error, retry }: ErrorBoundaryProps) {
return (
<View style={styles.screen}>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.message}>{error.message}</Text>
<Pressable style={styles.button} onPress={retry}>
<Text style={styles.label}>Try again</Text>
</Pressable>
</View>
);
}
export default function Profile() {
return (
<View style={styles.screen}>
<Text>The profile screen</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24, gap: 12 },
title: { fontSize: 20, fontWeight: "bold" },
message: { color: "#666", textAlign: "center" },
button: { backgroundColor: "#5b4bf5", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8 },
label: { color: "white", fontWeight: "bold" },
});When the Profile screen throws while rendering, the user sees the friendly message and a button instead of a crash. Error boundaries catch errors in rendering. They do not catch mistakes inside event handlers or promises, so use try and catch there, as in the data fetching lesson.
In a released app you will not see your users' crashes unless you collect them. Add a crash reporting service, such as Sentry, which records the error and the steps that led to it.
Why write tests
A test is code that runs your code and checks the result. You change something, run the tests, and know within seconds whether you broke anything. Tests are most useful for logic that is easy to break, such as calculations, forms and anything with several states.
Expo's testing guide recommends this setup:
- Jest, the test runner.
- jest-expo, a preset that fakes the native parts of the Expo libraries so tests can run on your computer without a phone.
- React Native Testing Library, which renders a component and lets you look for text and press buttons the way a user would.
Install and configure them:
Terminal
npx expo install jest-expo jest @types/jest --dev
npx expo install @testing-library/react-native --devJSON
{
"scripts": {
"test": "jest --watchAll"
},
"jest": {
"preset": "jest-expo"
}
}A first test
Here is a small component, and a test that uses it like a person would:
React (TSX)
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
export function Counter() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Pressable onPress={() => setCount(count + 1)}>
<Text>Add one</Text>
</Pressable>
</View>
);
}React (TSX)
import { render, screen, userEvent } from "@testing-library/react-native";
import { Counter } from "./Counter";
test("starts at zero", async () => {
await render(<Counter />);
expect(screen.getByText("Count: 0")).toBeTruthy();
});
test("adds one each time the button is pressed", async () => {
const user = userEvent.setup();
await render(<Counter />);
await user.press(screen.getByText("Add one"));
await user.press(screen.getByText("Add one"));
expect(screen.getByText("Count: 2")).toBeTruthy();
});Walk through it:
renderdraws the component in a test environment. In current versions of the library it is asynchronous, so youawaitit.screenis how you look at what was drawn.getByTextfinds an element by the words it shows, and throws if it is not there, which fails the test.userEvent.setup()creates a simulated user.await user.press(element)presses like a finger would.expect(...).toBeTruthy()checks that the thing exists.
Name the file ending in .test.tsx (or put it in a folder called __tests__) so Jest finds it. Run the tests with npm test.
Quick quiz
Why do component tests find elements by their text instead of by internal details?
What to test, and what not to
- Do test behaviour people depend on: a form shows an error for a bad email, a total adds up, a button is disabled until the form is valid.
- Do test pure logic separately, as plain functions, which is the easiest kind of test to write.
- Do not test the libraries. You do not need to prove that React Native draws a
View. - For whole journeys, such as sign up, add to cart and pay, use an end-to-end tool that drives a real app on a device or emulator. Expo's guide recommends tools such as Maestro for that, because unit tests cannot see a real phone.
Try it yourself
Take the sign-up form from the state lesson and write three tests: the button is disabled when the form is empty, it is enabled after a valid email and password are typed, and pressing it shows the welcome alert (replace Alert.alert with a function passed as a prop, so you can check it was called). Then break the form on purpose and watch a test fail.
Summary
- Debug with React Native DevTools (press j in the Expo terminal) and the developer menu (press m), and shrink problems until the cause is clear.
- Export an
ErrorBoundaryfrom a route file so a rendering crash shows a retry screen instead of closing the app, and collect real crashes with a reporting service. - Test components with Jest, jest-expo and React Native Testing Library by finding text and pressing buttons like a user, and use end-to-end tests for whole journeys.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.