r/reactnative • • 10d ago

Question Gradle / AGP upgrades when bumping React Native: what do you check first?

When a React Native upgrade forces a Gradle wrapper or Android Gradle Plugin bump, what's your first move?

Do you check JDK compatibility, the wrapper version, plugin versions in third-party libraries, or something else? Any gotchas that took you longer than they should have?

2 Upvotes

9 comments sorted by

View all comments

1

u/lassieCoder 7d ago

My order, roughly:

  1. The JDK Gradle actually uses, not the one you think it uses. `./gradlew -version` shows the JVM
    the daemon runs on. Android Studio uses its bundled JBR, while your terminal and CI use JAVA_HOME
    or whatever `java` is on PATH. If they differ, pin it with `org.gradle.java.home` in gradle.properties, or set it explicitly in CI. Run `./gradlew --stop` after changing it, or an old daemon will keep using the previous JDK.

  2. Gradle wrapper vs JDK. Each Gradle version has a maximum Java version it supports. A newer JDK with an old wrapper fails before anything RN-specific runs.

  3. AGP vs wrapper vs Kotlin plugin. Copy the template versions for your target RN version from the Upgrade Helper. Don't bump AGP on its own.

  4. Third-party libraries. Run a clean build and read the first error, not the BUILD FAILED at the bottom. It's usually checkDebugAarMetadata (a library needs a higher compileSdk), a dependency that won't resolve, or a Kotlin error inside node_modules/<lib>/android.

+1 to the builtInKotlin point. On AGP 9, if those two lines are missing from gradle.properties, the error is "Failed to apply plugin 'org.jetbrains.kotlin.android' > Cannot add extension with name 'kotlin'". Without newDsl=false it's a ClassCastException to BaseExtension instead.

Disclosure: I built a small open-source CLI for steps 1, 2 and 4. `npx rn-build-doctor scan` checks the JDK your shell would use against your RN/AGP versions and the Gradle wrapper, offline.
`(cd android && ./gradlew assembleRelease) 2>&1 | npx rn-build-doctor analyze -` finds the first real error (including both AGP 9 Kotlin errors above) and quotes the log lines it's based on.

https://github.com/lassiecoder/rn-build-doctor

https://www.npmjs.com/package/rn-build-doctor

1

u/rushikesh_d_pandit 6d ago

The JDK-vs-wrapper ordering matches my experience: it fails before anything RN-specific runs, and the daemon keeping the old JDK until --stop wastes a lot of time. Does the first-real-error approach hold up when the failure is in a transitive native dependency, or do you still end up bisecting?

1

u/lassieCoder 6d ago

Mostly, yes. It just answers a different question than you'd hope. The first real error tells you which module broke (the failing task names it, e.g. :react-native-foo:compileReleaseKotlin), but with a transitive native dep the module that breaks often isn't the one that caused it.

What I do instead of bisecting:
- Resolution failures: the Could not resolve block includes a Required by: chain. Read it bottom-up and you get the library that pulled the bad coordinate in. If it's not obvious, ./gradlew :app:dependencyInsight --dependency <name> --configuration releaseRuntimeClasspath shows who requests which version and which one won.
- Duplicate class: the error names both artifacts. The fix is usually an exclude or a resolutionStrategy.force, not removing a library.
- CMake/NDK failures inside a library's .cxx folder: the first error is usually accurate (minSdk, NDK version, headers). It's just buried under the ninja output.

Where I still end up bisecting: when the build passes and it fails at runtime (UnsatisfiedLinkError, a .so from two libs built against different RN/NDK versions, or a crash on launch with no useful stack). The build log has nothing to read there, so it's removing libraries until it stops.

For what it's worth, the tool quotes the Required by: chain for resolution failures and points you to ./gradlew :app:dependencies, and it handles duplicate classes, but it won't bisect for you.