Million

React Native Doctor

We built React Doctor to give React codebases a single health score. Today we're doing the same thing for mobile.

React Native Doctor scans your React Native and Expo projects and gives you a 0-100 score, plus the exact diagnostics behind it. It focuses on the three things mobile teams get bitten by most: performance, accessibility, and design.

Run your first scan

Point it at the root of a React Native or Expo project:

npx react-doctor@latest

It figures out your React Native version, Expo vs bare workflow, the JS engine (Hermes or JSC), and your navigation and state libraries before it scans. So the rules that run are the ones that actually apply to your app.

The three things that matter on mobile

Performance

Phones are slow and users notice. A dropped frame on the web is invisible; on a mid-range Android it's a visible stutter. The usual culprits are non-virtualized lists that mount every row at once, images with no caching that reload while you scroll, and work that runs on every render instead of once.

These are the issues that make an app feel cheap, so they're the first thing we scan for.

Accessibility

A huge share of accessibility bugs in React Native come from one thing: a touchable with no label. VoiceOver and TalkBack then announce "button" with no name, and the screen is unusable for anyone who can't see it.

Tap targets are the other big one. A 28x28 icon button is easy to miss for anyone, and fails the 44x44 minimum that both iOS and Android expect.

Design

Consistency and platform polish are what make an app feel built, not assembled. Shadow styles that silently don't render on Android, spacing that doesn't match your scale, and colors that skip your theme all read as "unfinished" even when nothing is broken.

What a scan looks like

React Native Doctor groups findings by concern and labels each one with a clear severity, so you always know what to fix first:

$ npx react-doctor@latest

React Native Doctor · Expo SDK 52 · Hermes · TypeScript · 38 files

Performance
  warn   app/feed/Feed.tsx:31       react-doctor/rn-no-scrollview-mapped-list
         <ScrollView>{posts.map(...)}</ScrollView> builds every row up front, so
         a long feed mounts hundreds of views and scrolling stutters. Use a
         virtualized list like FlashList or FlatList.

  warn   app/feed/Avatar.tsx:3      react-doctor/rn-prefer-expo-image
         Image from react-native has no caching, so avatars reload on every
         scroll. Switch to <Image> from expo-image.

Accessibility
  error  app/feed/LikeButton.tsx:8  react-doctor/rn-touchable-missing-label
         <TouchableOpacity> has no accessibilityLabel, so screen readers announce
         "button" with no name. Add accessibilityLabel="Like".

  warn   app/feed/Row.tsx:42        react-doctor/rn-tap-target-too-small
         Touch target is 28x28. iOS and Android both expect at least 44x44.
         Increase the size or add hitSlop.

Design
  warn   app/feed/Row.tsx:19        react-doctor/rn-no-legacy-shadow-styles
         shadowColor and shadowOffset don't render on Android. Use boxShadow so
         the card shadow shows up on both platforms.

  5 issues · 1 error · 4 warnings                             Score 74 / 100

Errors vs warnings

Severity is the whole point of the score, so we keep it simple.

  • error means it ships a real bug. Jank a user can feel, a control a screen reader can't use, a crash on Hermes. Fix these before you merge.
  • warn means it's real but lower stakes. Polish, consistency, a small gap. Fix it when you can, or suppress it when the code is intentionally unusual.

How to fix one

Start with the performance finding that costs the most. This feed renders by mapping an array inside a ScrollView:

<ScrollView>
  {posts.map((post) => (
    <Row key={post.id} post={post} />
  ))}
</ScrollView>

A ScrollView has no virtualization. A 500-post feed builds 500 rows on the first render and keeps every one mounted, on screen or not. On a mid-range Android that is exactly where the scroll stutter comes from.

Switch to a virtualized list. It only mounts the rows on screen and recycles them as you scroll:

import { FlashList } from "@shopify/flash-list";

<FlashList
  data={posts}
  renderItem={({ item }) => <Row post={item} />}
  keyExtractor={(item) => item.id}
/>;

Now the feed mounts a handful of rows instead of hundreds, and scrolling stays smooth no matter how long the list gets. Every diagnostic comes with this kind of recommendation, so you're never staring at a rule name wondering what it wants.

Built for agents

We care a great deal about speed, and that includes the speed of fixing things. The output is deterministic and machine-readable, so you can wire it into CI and let a coding agent read the findings and fix them:

npx react-doctor@latest --json

Scan only what changed

When you're on a branch, scan just the diff:

npx react-doctor@latest --diff main

This is just the start. We're expanding the rule set, adding more Expo-specific checks, and putting together a mobile leaderboard. Try it on your app and tell us what it finds.