Dependencies and Lockfiles

Why npx expo install picks the version your SDK needs, what a committed lockfile guarantees, and how to audit a dependency tree.

Syntaxnpx expo install <package> | npx expo install --check | npx expo install --fix

The React Native docs put it bluntly: React Native is not backwards compatible. A package that contains native code has to match the React Native version in your project. Left alone, npm installs the newest published version of that package, which was probably built against a newer SDK than yours.

Install through Expo

npx expo install expo-camera is a drop-in replacement for npm install expo-camera. Same registry, same lockfile, same package.json entry. The difference is that Expo looks the package up in its list of known-good version combinations for your SDK and installs that version instead of the latest. A package with no native code, such as a date formatter, can go through npm directly.

Two flags audit what is already installed. npx expo install --check reports packages on the wrong version and exits non-zero, which is what continuous integration needs. npx expo install --fix corrects them without prompting.

The lockfile

Commit it. EAS Build installs with your package manager's immutable flag, meaning npm ci or yarn --frozen-lockfile, so the build gets exactly the tree you tested; the environment variable EAS_NO_FROZEN_LOCKFILE set to 1 disables that, and is a last resort.

What a package drags in

npm ls react-native-reanimated prints every copy in the tree, including copies pulled in by other libraries. Two versions of one native library is a link error, not a warning. npx expo-doctor validates your dependencies against the React Native directory and flags packages that do not support this SDK.

Example

Example · tsx
// Not a component: a release check you can run over package.json.
// A native package pinned loosely will drift onto a version built for a
// different SDK the next time someone deletes node_modules.

type Ranges = Record<string, string>;

// "*", "latest" and open-ended ranges let the resolved version move on its
// own. "^57.0.0" is what expo install writes for the SDK itself, so it passes.
const UNPINNED = /^(\*|latest|>=?)/;

export function looseNativeRanges(deps: Ranges): string[] {
  return Object.entries(deps)
    .filter(([name]) => name === "react-native" || name.startsWith("expo-"))
    .filter(([, range]) => UNPINNED.test(range))
    .map(([name]) => name)
    .sort();
}

const dependencies: Ranges = {
  expo: "^57.0.0",
  "expo-camera": "latest",
  "expo-font": "*",
  react: "19.2.3",
  "react-native": ">=0.86.0",
};

console.log(looseNativeRanges(dependencies));
// Output: [ 'expo-camera', 'expo-font', 'react-native' ]

When to use it

  • Adding the camera to an existing app without landing on a package version built for a different SDK.
  • Making a teammate's clone of the repository install the exact tree that the last green build used.
  • Clearing an npm audit finding that comes from a package you never installed yourself.
  • Finding out why a release build fails to link after a library update that worked in development.

More examples

Which command for which package

The daily decision: expo install for anything with a native side, npm for anything that is only JavaScript. Both write to the same package.json and the same lockfile, so there is no switching cost and no harm in using expo install for everything. The last two lines are the part people skip. A JavaScript reload cannot add a native module to a binary that was compiled without it.

Example · bash
# Native code inside the package: let Expo choose the version for this SDK.
npx expo install expo-camera
npx expo install expo-image-picker react-native-safe-area-context

# Pure JavaScript, no native side: npm is fine, nothing to match.
npm install zod

# Report wrong versions and exit non-zero. The CI form.
npx expo install --check

# Correct them without prompting. The local form.
npx expo install --fix

# Every copy of a native library in the tree, including ones other
# packages installed. Two copies will not link.
npm ls react-native-safe-area-context

# Known vulnerabilities in what ships, ignoring the build-time tree.
npm audit --omit=dev

# After any install that added native code, rebuild rather than reload.
npx expo prebuild --clean
eas build --profile development --platform ios

Pinning a transitive package with overrides

overrides is npm's way to force one version of a package you never installed directly, which is how you clear a vulnerability report against something three levels down the tree without waiting for its parent to publish a fix. Yarn spells the same idea resolutions, pnpm spells it pnpm.overrides. Use it for JavaScript packages. Overriding a native package's version is how you end up with code compiled against headers that no longer match, and expo-doctor will report the mismatch rather than the crash it causes.

Example · json
{
  "dependencies": {
    "expo": "^57.0.0",
    "react": "19.2.3",
    "react-native": "0.86.0"
  },
  "overrides": {
    "semver": "^7.0.0"
  }
}

Catching drift in continuous integration

A pull request check that refuses dependency drift before it reaches a build machine. npm ci is the strict sibling of npm install: it deletes node_modules, installs the lockfile verbatim, and errors instead of quietly updating the lockfile. That is the same behaviour EAS Build uses by default, so passing here means the cloud build resolves the same tree. Pin the Node version to the one the SDK requires; a mismatch there produces errors that look like code problems.

Example · yaml
# .github/workflows/deps.yml
name: deps
on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22.13.0
          cache: npm

      # npm ci installs from package-lock.json exactly, and fails if
      # package.json and the lockfile disagree.
      - run: npm ci

      # Non-zero exit when a package is on a version this SDK does not expect.
      - run: npx expo install --check

      - run: npx expo-doctor

Discussion

  • Be the first to comment on this lesson.