The Problem That Didn't Make Sense
I was working on an Expo-managed React Native application targeting iOS. Everything looked normal until I noticed a version mismatch involving expo-file-system.
My package.json contained:
{
"expo": "^54.0.0",
"expo-file-system": "^55.0.16"
}
However, the Expo SDK 54 documentation showed:
expo-file-system ~19.0.23
At first glance, this looked wrong. How could the package be version 55.0.16 in my project while the documentation recommended 19.0.23?
The confusion became even greater when I ran:
pod install
and saw:
Installing ExpoFileSystem 19.0.23 (was 55.0.16)
The build succeeded.
At that moment I realized I didn't actually understand how Expo packages, native modules, CocoaPods, and version compatibility worked together.
My Initial Assumption
Like many JavaScript developers, I assumed:
npm install expo-file-system
means:
Install the package and everything needed for it.
This mental model works reasonably well for pure JavaScript libraries such as:
npm install axios
npm install zod
npm install date-fns
There is no native code involved. The package version is the package version.
Expo modules are different.
Understanding the Layers
An Expo package actually exists in multiple worlds.
Expo SDK
│
├── JavaScript Package
│ └── expo-file-system
│
├── Native iOS Module
│ └── ExpoFileSystem Pod
│
├── Native Android Module
│ └── Gradle Module
│
└── Expo Compatibility Matrix
When you use:
npm install expo-file-system
npm only knows about the JavaScript package published to npm.
It does not know:
Which Expo SDK you're using
Which React Native version you're using
Which native iOS pod version is compatible
Which Android native implementation should be used
Its job is simply:
Install the latest package matching the version range.
Nothing more.
What npx expo install Actually Does
Expo provides its own installation command:
npx expo install expo-file-system
This command behaves differently.
Before installing anything, Expo checks:
Current Expo SDK Version
↓
Compatible Package Version
↓
Install That Version
In my case:
Expo SDK 54
↓
expo-file-system ~19.0.23
↓
Install 19.0.23
Instead of grabbing the newest package available on npm, Expo chooses the version tested and validated against the current SDK.
This is why Expo documentation almost always recommends:
npx expo install <expo-package>
instead of:
npm install <expo-package>
Then Why Did pod install Fix It?
This was the part that confused me the most.
I expected CocoaPods to simply install whatever was in package.json.
That's not what happens.
During:
pod install
Expo autolinking runs.
Autolinking scans installed Expo modules and reads their native configuration files.
A simplified view looks like:
node_modules
↓
Expo Package
↓
Podspec
↓
CocoaPods
↓
Native Pod Installation
When CocoaPods encountered the Expo File System module, it discovered that the native pod version expected for SDK 54 was:
ExpoFileSystem 19.0.23
and corrected the native dependency accordingly.
That's why I saw:
Installing ExpoFileSystem 19.0.23 (was 55.0.16)
The native side was effectively protecting itself from an incompatible version.
The Important Lesson
The successful build was actually misleading.
The build succeeding does not mean the dependency setup is healthy.
I still had a mismatch between:
JavaScript Layer
expo-file-system 55.x
and
Native Layer
ExpoFileSystem 19.x
A future build, CI pipeline, runtime feature, or TypeScript API could easily break because the JavaScript package and native implementation are expecting different things.
The fact that CocoaPods repaired the native side doesn't mean the project is correctly configured.
The Rule I Follow Now
Whenever I add an Expo package:
npx expo install expo-file-system
npx expo install expo-camera
npx expo install expo-location
Whenever I add a normal JavaScript library:
npm install axios
npm install zod
npm install lodash
A simple way to remember it is:
If the package belongs to the Expo ecosystem, let Expo decide the version. If it's a pure JavaScript dependency, let npm decide.
A Helpful Command I Learned
If you inherit an existing Expo project and suspect version drift, run:
npx expo install --fix
Expo will inspect installed Expo packages and align them with the currently installed SDK.
Follow it with:
npx expo doctor
to identify remaining compatibility issues.
Final Mental Model
The mental model that finally made everything click for me was:
npm install
=
Install package
expo install
=
Find compatible version
+
Install package
pod install
=
Install native iOS dependencies
Each tool solves a different problem.
The mistake is assuming npm understands Expo SDK compatibility. It doesn't.
Expo does.
And that small distinction is the reason a seemingly mysterious pod install ended up teaching me how Expo's dependency system actually works.
Top comments (0)