shipcheck

Why React Native and Expo apps get rejected differently

Updated September 6, 2026 · published by Baker Ventures LLC · sources cited inline

Because in a React Native or Expo project, the thing that gets you rejected is usually not in your source code. It is in node_modules, or in a ios/ directory that a build server regenerates from scratch, or in an Info.plist key that a config plugin injected on behalf of a package you installed and forgot.

Generic iOS compliance advice assumes you wrote the code, that the Xcode project is the source of truth, and that a permission string exists because a human typed it. None of those hold here. That mismatch is why "I read the App Store Review Guidelines carefully" is not sufficient preparation for shipping an Expo app, and why a checker that does not understand the dependency tree will miss the findings that matter.

The four RN and Expo specific causes

1. Transitive dependencies you never chose

Apple's privacy-manifest requirement is per-bundle: each executable or dynamic library that uses a required reason API must ship a manifest in its own bundle. Your app's manifest does not cover a framework.

In a native Swift project that is a short list you curated. In a React Native project, one @react-native-firebase/app install pulls in gRPC, which pulls in nanopb, Abseil, Protobuf, BoringSSL, GoogleUtilities and leveldball of which are on Apple's required-SDK list. You did not choose any of them and most developers could not name them.

And the one that surprises everyone: hermes is on Apple's list. React Native's default JavaScript engine. That makes this a near-universal RN concern rather than a Firebase concern. Which of your dependencies need a manifest.

2. The native project is generated, not authored

With Expo's continuous native generation, ios/ and android/ are build artefacts. They are regenerated from app.json, app.config.js and your config plugins.

The failure mode is specific and expensive: you get rejected, you open ios/YourApp/Info.plist, you fix the key, you verify locally, you push, EAS runs prebuild, your fix is gone, and you are rejected again for the identical reason. Two cycles, several days, and the change was correct both times.

Anything that must survive a build has to be expressed in app config or a config plugin. A checker that reads only the generated ios/ directory is checking a file that will not exist in the same form on the build server.

3. Config plugins inject permissions on your behalf

Install a package that needs the camera and a config plugin typically adds NSCameraUsageDescription during prebuild. That is a genuinely good feature — until you remove the feature and keep the package.

Now your Info.plist declares a purpose string for a capability the app never exercises. App Review reads the shipped Info.plist, not your intent. Apple's data-minimisation expectations under Guideline 5.1.1 make "requests a permission it does not need" a live rejection reason, and Guideline 2.5.4 specifically rejects apps that declare the location UIBackgroundModes key without a genuine persistent-location feature.

The uncomfortable version of this: your permission surface is a function of your dependency list, and most teams have never actually read it.

4. Expo's own tooling is honest about a gap

Expo's Apple privacy guide states that "Apple does not correctly parse all the PrivacyInfo files included by static CocoaPods dependencies," and recommends manually walking node_modules for PrivacyInfo.xcprivacy files and aggregating the values yourself. Expo also flags that guide as still in development.

That is the actual state of the art: hand-auditing a tree of several hundred packages against a list Apple updates, before every submission. It is exactly the kind of task that should not be done by a human, and exactly the kind that a generic iOS checker has no reason to implement.

What a generic checker catches, and what it misses

FindingGeneric iOS checkerNeeds RN/Expo understanding
Screenshots mention Android (2.3.10)Yes
Placeholder text or dead privacy-policy linkYes
Missing in-app account deletion (5.1.1(v))Yes
Subscription paywall disclosure (3.1.2)Yes
Required-reason API used by hermesRarelyYes
Transitive pod on Apple's SDK list with no manifestNoYes
Info.plist key injected by a config plugin for a removed featureNoYes
Fix applied to ios/ that prebuild will overwriteNoYes
Permission string sourced from an Expo moduleNoYes
Play foreground-service type mismatched to a dependencyNoYes

The right way to read that table: framework-independent issues are well served by lots of tools, including free ones. The bottom half is where RN and Expo apps actually lose cycles, and it is the half that requires reading node_modules, Podfile.lock, app config and config plugins together.

What to check before every submission

  1. Walk node_modules for PrivacyInfo.xcprivacy and diff against Apple's SDK list.
  2. Read the Info.plist that prebuild produces, not the one in your repository, and justify every purpose string against a real feature.
  3. Check UIBackgroundModes against features that genuinely need them.
  4. Confirm every fix is expressed in app config or a plugin, not in a generated directory.
  5. Check Podfile.lock for resolved versions of transitive pods on Apple's list.
  6. Re-check after every dependency bump. This surface changes when your lockfile changes, not when your code does.

shipcheck runs exactly this, inside your own Claude Code session, against your local project — no account, no repository upload, first scan free with real findings, with the guideline clause quoted next to each one. What it checks.

Questions and answers

Do React Native apps get rejected more often than native apps?

There is no public data comparing rejection rates by framework, so any claim in either direction is speculation. What is specific to React Native and Expo is the type of rejection - configuration issues originating in dependencies and generated native projects rather than in code the developer wrote.

Why does my Expo app have permission strings I never added?

Config plugins add them. Installing a package that needs camera or location access typically injects the corresponding Info.plist purpose string during prebuild. If you later remove the feature but not the package, the string stays and App Review asks why your app needs a permission it never uses.

Why do my ios directory changes keep disappearing?

Because with Expo's continuous native generation the ios and android directories are build artefacts, regenerated from app.json and config plugins on every prebuild. Any hand edit is overwritten. Changes must be expressed in app config or a config plugin to survive a build.

Does Expo handle privacy manifests for me?

Partially, and Expo's own guide is candid about the limits. It states that Apple does not correctly parse all PrivacyInfo files included by static CocoaPods dependencies, and recommends manually walking node_modules and aggregating the values yourself.

Is a generic App Store compliance checker enough for a React Native app?

It will catch the metadata and content issues, which are framework-independent. It will generally miss the configuration issues that dominate RN and Expo rejections, because those require understanding how Expo modules pull permissions and privacy declarations in transitively.

Cite this pageshipcheck. “Why React Native and Expo apps get rejected differently.” Baker Ventures LLC, September 6, 2026. https://shipcheck.bakerventuresstudio.com/why/why-react-native-apps-get-rejected-differently/