In an Expo project, when would you embed a custom font with the expo-font config plugin instead of loading it with useFonts?
answer
- build time versus runtime
- plugin needs a development build
- useFonts works in Expo Go and on web
- returns [loaded, error]
- hold the splash until loaded or error
basics
~20 sEmbed with the expo-font config plugin for native builds: the font ships in the binary and is usable on the first frame. Use useFonts for Expo Go or web, accepting an async load and a render gate.
solid answer
~40 sBoth come from `expo-font`. The **config plugin**, listed in `plugins` in the app config, copies the files into the native projects at prebuild, so the font is part of the binary and available on the first frame; Expo recommends it. Its costs are that it needs a development build rather than Expo Go, does not apply on web, and changing fonts means a rebuild. **`useFonts`** loads a map of family names to sources at runtime and returns `[loaded, error]`; it works in Expo Go and on web, but the root layout must render nothing, usually keeping the splash screen up with `expo-splash-screen`, until `loaded` or `error` is set. For a shipped native app I embed; for a quick Expo Go prototype or a web target I use `useFonts`.
code
tsx · 25 linesimport { useEffect } from 'react';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { Stack } from 'expo-router';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
'BrandSans-Regular': require('../assets/fonts/BrandSans-Regular.otf'),
'BrandSans-Bold': require('../assets/fonts/BrandSans-Bold.otf'),
});
useEffect(() => {
if (loaded || error) {
SplashScreen.hideAsync();
}
}, [loaded, error]);
if (!loaded && !error) {
return null;
}
return <Stack />;
}go deeper
Recall the two routes in expo-font: a config plugin that embeds fonts at build time, and a useFonts hook that loads them at runtime and returns loaded and error.
Explain the tradeoff: embedding needs a development build and skips web but gives first-frame fonts; useFonts works in Expo Go and web but needs a splash-screen gate.
Show production judgment: embed for native release builds, keep useFonts for web, gate on error so a failed font never strands the splash, and verify with getLoadedFonts.
Weigh font changes as native changes: an embedded font cannot ride a JavaScript update, so decide which typography changes must wait for a store build.
## Two ways expo-font makes a font available `expo-font` is the Expo SDK package for custom fonts, and it offers two independent routes. Both end with the same usage, a `fontFamily` string on a `Text` style, but they differ in **when** the font becomes known to the platform: - **Build time**: the `expo-font` **config plugin** embeds font files into the native Android and iOS projects during prebuild. - **Runtime**: the **`useFonts`** hook (or the imperative `loadAsync`) loads font files after the JavaScript starts and registers them with the native font system. ## The config plugin: fonts inside the binary You list the plugin in the `plugins` array of `app.json` or `app.config.ts` with a `fonts` array of paths relative to the project root, and optionally separate `android` and `ios` sections. At prebuild it: 1. on iOS, adds each file to the app target and to `UIAppFonts` in `Info.plist`; 2. on Android, copies `.ttf` and `.otf` files into `assets/fonts`, or, for entries written with the object syntax (`fontFamily` plus `fontDefinitions` with `path`, `weight` and optional `style`), into `res/font` with a generated XML font family registered at app start. Benefits, as Expo's own guide lists them: the font is available the moment the app starts, no asynchronous loading code is needed, and every install has it because it ships in the binary. That is why Expo calls it the recommended method. The limits are real too: - it **does not work in Expo Go**, because Expo Go is a prebuilt app whose native project you cannot change; you need a development build; - config plugins run only for native platforms, so **web still needs `useFonts`**; - adding or replacing a font is a native change, so it needs a **new build**, not just an update to the JavaScript. ## useFonts: fonts loaded at runtime `useFonts(map)` takes an object whose keys become `fontFamily` names and whose values are sources, usually `require('./assets/fonts/BrandSans-Regular.otf')`, and returns a tuple **`[loaded, error]`**. Under the hood it calls `loadAsync` once in an effect; the font map is not reloaded if you change it later. Because loading is asynchronous, any `Text` rendered before it finishes would be laid out in the system font and could shift when the brand font arrives. The standard pattern therefore: 1. calls `SplashScreen.preventAutoHideAsync()` from `expo-splash-screen` at module scope; 2. calls `useFonts` in the root layout; 3. returns `null` while neither `loaded` nor `error` is set; 4. calls `SplashScreen.hideAsync()` in an effect once either is set. Gating on `error` as well as `loaded` matters: if a file fails to load, the app should continue with system fonts rather than sit on the splash screen forever. `useFonts` works in **Expo Go and on web**, where it injects an `@font-face` rule, and it can take a remote URL as a source. That flexibility is its whole case. ## Choosing between them | | Config plugin | `useFonts` | |---|---|---| | When the font is known | at build time | after JS starts | | Loading code | none | hook plus render gate | | Expo Go | no, needs a development build | yes | | Web | no | yes | | Changing fonts | new native build | JS change | | First-frame text | brand font | blank or splash until loaded | Many production apps use **both**: the plugin for iOS and Android, and `useFonts` guarded to the web build. `getLoadedFonts()` from `expo-font` lists fonts from either route, which is a handy debugging check. ## Pitfalls interviewers look for - Rendering the navigator before `useFonts` resolves, so the first screen flashes the system font and re-measures its text. - Gating only on `loaded`, which strands the splash screen when a font fails. - Expecting a plugin-embedded font in Expo Go and concluding the configuration is broken. - Calling `useFonts` in many screens. It is harmless, because a family that is already loaded resolves at once, but only the root layout should decide when the app is ready to render. - Assuming the plugin's `fontFamily` naming rules are the same on both platforms: iOS always reads the family from the file, Android uses the file name or the name you give in the object syntax.
- Why does an Expo root layout gate on loaded || error instead of just loaded?`useFonts` returns `[loaded, error]`. If a font file fails to load, `loaded` never becomes true, so a gate on `loaded` alone would keep returning `null` and the splash screen would never hide. Including `error` lets the app continue in system fonts, and the error can be reported in development.
- A teammate adds a font through the expo-font config plugin and it does not show in Expo Go. Is the configuration wrong?Not necessarily. The config plugin edits the native projects at prebuild, and Expo Go is a prebuilt app whose native project cannot change, so plugin-embedded fonts never reach it. Build and install a development build with `npx expo run:android` or an EAS build, or load the font with `useFonts` if Expo Go support matters.
saying these in an interview costs you the question
- The expo-font config plugin makes fonts available in Expo Go.
- useFonts returns a Promise that you await before rendering.
- Gating the root layout only on loaded is fine because fonts never fail.
- Config plugins also embed the font into the web build.
- Changing a plugin-embedded font only needs a JavaScript update.