skip to content

In an Expo app, how do you read app config values at runtime through Constants.expoConfig and extra, and how does that differ from EXPO_PUBLIC_ variables?

level: middleimportance: should knowfreq 40%

answer

  1. expo-constants, nullable getter
  2. extra carries arbitrary data
  3. never import app.json directly
  4. some fields filtered out of public config
  5. npx expo config --type public

basics

~20 s

Import Constants from expo-constants and read Constants.expoConfig?.extra, which holds the public part of the resolved app config. EXPO_PUBLIC_ variables are literals inlined into your code; extra is data carried in the app's embedded config or manifest.

solid answer

~40 s

`Constants.expoConfig` from `expo-constants` exposes the public part of the resolved app config, and the `extra` key is where you put arbitrary values, often computed in `app.config.ts` from environment variables. Read it defensively, `Constants.expoConfig?.extra?.apiUrl`, because the getter can return `null`. Do not `import` `app.json`: that bundles the raw file, not what a dynamic config resolved to. A few fields are filtered out (`hooks`, `ios.config`, `android.config` and the update code-signing fields), and `npx expo config --type public` shows exactly what is exposed. The difference from `EXPO_PUBLIC_` variables: those are replaced by literals in your own source at bundle time, while `extra` travels as config data in the embedded config, the dev server's manifest or an update's manifest. Both are public; neither may hold secrets.

code

typescript · 8 lines
typescript
import Constants from 'expo-constants';

type Extra = { apiUrl?: string; supportPhone?: string };

const extra = (Constants.expoConfig?.extra ?? {}) as Extra;

export const apiUrl = extra.apiUrl ?? process.env.EXPO_PUBLIC_API_URL;
export const supportPhone = extra.supportPhone;

go deeper

for a junior

Know that Constants.expoConfig.extra from expo-constants holds custom config values, and that it may be null.

for a middle

Explain where the runtime copy comes from in development, in a release build and under an update, and how extra differs from inlined EXPO_PUBLIC_ literals.

for a senior

Verify what ships with npx expo config --type public, keep secrets out of both mechanisms, and trace stale values to the build or update that carried them.

for a principal

Standardise one typed environment object per app so variants and libraries read the same source instead of scattered literals.

## Where Constants.expoConfig comes from `expo-constants` is the Expo module that exposes system and app information to JavaScript. Its `expoConfig` property returns the **public app config**: the resolved result of `app.json` plus any `app.config.ts`, minus a few filtered fields. Which copy you receive depends on how the app was launched: - **A release build running its embedded bundle** reads a config file that `expo-constants` generates and packages during the native build. - **A development build connected to `npx expo start`** reads the manifest the dev server serves, which is re-evaluated as Metro reloads. - **An app running an EAS Update** reads the config stored in that update's manifest, captured when the update was exported. That is why the getter is typed as nullable: there are launch situations without a manifest, and code should read `Constants.expoConfig?.extra`. ## The extra key `extra` is the app config's free-form slot. Anything you put there is passed to your app: ```ts // app.config.ts export default ({ config }) => ({ ...config, extra: { apiUrl: process.env.API_URL, supportPhone: '+1 555 0100' }, }); ``` Because `app.config.ts` runs in Expo CLI's Node process, it can read **unprefixed** environment variables and copy chosen ones into `extra`. That is the older way of passing environment values to an app, and it still works. ## What is filtered out, and how to check The public config drops fields that are meant for tooling only: - `hooks` - `ios.config` and `android.config` - `updates.codeSigningCertificate` and `updates.codeSigningMetadata` Everything else is visible at runtime, and therefore extractable from the app package. Run `npx expo config --type public` to print exactly what will be embedded and exposed. Separately, **do not import `app.json` or `app.config.js` into app code**: an import bundles the raw file, which misses whatever a dynamic config computed and drags the whole file into the bundle. ## extra versus EXPO_PUBLIC_ | | `Constants.expoConfig.extra` | `process.env.EXPO_PUBLIC_*` | |---|---|---| | Mechanism | config data in a manifest or embedded config | literal text substituted into your code | | Where values come from | whatever `app.config.ts` computes | `.env` files and the bundling environment | | Reachable from `node_modules` | yes, any code can read `Constants` | no, library code is not rewritten | | Needs a module | `expo-constants` | none | | Visibility | public | public | The Expo environment-variables guide suggests moving references from `Constants.expoConfig.extra` straight to `EXPO_PUBLIC_` variables when the value is only used in your code. `extra` still earns its place when: 1. the value is **derived** from several inputs in the config itself, such as a variant name; 2. a **library** needs to read it, since libraries are not inlined; 3. you want **one typed object** describing the environment rather than scattered literals. ## Reading it in practice A tidy pattern is one small module that owns the read: 1. Import `Constants` from `expo-constants` once, in a file such as `env.ts`. 2. Cast `Constants.expoConfig?.extra` to a declared type, because `extra` is typed loosely. 3. Validate the fields you need and throw a clear error at startup if one is missing. 4. Export typed constants; screens and API clients import those, never `Constants` directly. This keeps the nullable getter and the loose typing in one place and gives tests a single seam to replace. It also means that when a car-sharing app's booking screen talks to the wrong host, there is exactly one file to inspect, and `npx expo config --type public` shows what that file will receive. ## Pitfalls - Reading `Constants.expoConfig.extra.apiUrl` without optional chaining crashes if the getter returns `null`. - Assuming a changed `extra` value reaches installed apps without a new build or update: the embedded copy is fixed at build time. - Putting a secret into `extra` because it is not in the source code: it is in the public config, which is inside the app.

  • A value added to extra shows up in development but not in the installed release build; why?
    In development `Constants.expoConfig` comes from the dev server's freshly evaluated manifest. A release build running its embedded bundle reads the config packaged when that native build was made, so the new `extra` value appears only after a new build, or in a newer update whose manifest carries it.
  • When is extra a better fit than an EXPO_PUBLIC_ variable?
    When a library in `node_modules` must read the value, because inlining skips `node_modules` while `Constants` is readable anywhere; or when the value is derived inside `app.config.ts` from several inputs, such as a variant name, and you want one typed object rather than several literals.

saying these in an interview costs you the question

  • Importing app.json is equivalent to reading Constants.expoConfig
  • Values in extra are private because they are not in the source
  • Constants.expoConfig is always non-null, no optional chaining needed
  • Every app config field is exposed through Constants.expoConfig
  • extra is re-read from the server on every launch