RORK LABJP
DEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per monthDEADLINE — From August 31, 2026, Google Play requires target API level 36 (Android 16) or higher for both new apps and updates to existing ones. Thirteen days leftEXTENSION — If you cannot make the date, the deadline extension form in Play Console buys you until November 1. The extension is not automatic, so the request itself has to land before August 31TARGET SDK — Even for Expo and React Native apps produced by builders like Rork, the targetSdkVersion is yours to verify. A template pinned to an older SDK will not meet the requirement on its ownPOLICY — The spam and minimum functionality policy has been tightened around high-quality features and content experience, which puts thin, mass-produced apps squarely in scopePRIVACY — You are expected to explain in detail what data is collected, how it is used, and whether it is shared, including analytics SDKs and advertising identifiers a builder wires in for youRORK — Where the original Rork emits React Native and Expo, Rork Max generates SwiftUI. There is a free tier to start with, and paid plans begin at $25 per month
Articles/Dev Tools
Dev Tools/2026-04-22Intermediate

Loading Custom Fonts Cleanly in Rork Apps — Preventing Startup Flicker (FOIT/FOUT) With expo-font

When you ship a Rork-generated app with a custom font, you often see a brief flash of the system font at launch — the classic FOIT/FOUT problem. This guide shows how to combine expo-font and expo-splash-screen to hold the splash screen until fonts are ready, with copy-paste-ready code.

Rork535expo-fontCustom FontsSplashScreenFOITFOUTReact Native227

When you first run a Rork-generated app on a real device, you may notice a subtle but annoying visual hitch: for a split second, every piece of text renders in the system font (San Francisco on iOS, Roboto on Android), and only then switches to your custom font. It's the mobile equivalent of FOIT (Flash of Invisible Text) and FOUT (Flash of Unstyled Text) that web developers have been fighting for years.

Telling Rork "use Noto Sans JP please" in a prompt is not enough to fix this. The flash comes from a timing gap between React rendering and font loading, so you have to control when the splash screen disappears. In this article, I'll walk through the pattern I reach for every time: hold the splash screen until fonts are ready, then hide it by hand. It's not glamorous, but it's the difference between an app that feels polished and one that feels rushed.

What's actually happening during those 200ms

A Rork project runs on Expo and React Native, so the startup pipeline roughly looks like this:

  1. The native side shows the splash screen.
  2. The JavaScript bundle loads and evaluates React.
  3. Hooks like useFonts kick off asynchronous font loading.
  4. React renders the root component.
  5. Fonts finish loading; text is re-rendered with the custom font.

The flicker lives between step 4 and step 5. If React renders before fonts are ready, the first paint uses the fallback system font, then swaps when the custom font arrives. The user perceives this as a flash.

Setting fontFamily in a Rork prompt doesn't solve this — you need to control rendering timing itself.

The fix in one sentence — keep the splash up until fonts are ready

By default, Expo hides the splash screen as soon as your root component mounts. We're going to switch that to manual mode: prevent auto-hide, wait for useFonts to resolve, and only then hide the splash. No splash-to-content gap means no flicker.

Install the two packages. Rork projects typically already have expo-font, but expo-splash-screen often needs to be added explicitly.

# Run at the root of your Rork project
npx expo install expo-font expo-splash-screen

You can ask Rork in chat to rewrite your entry file for this pattern, and it will produce something close to what I show below. That said, I've learned not to hand off the small details entirely to the AI — a few edge cases catch you off guard. Let's walk through the minimal robust version.

A working _layout.tsx you can drop in

If your Rork project uses Expo Router (the default), the cleanest place for this logic is app/_layout.tsx. Here's a version I've shipped in production several times.

// app/_layout.tsx
import { Stack } from "expo-router";
import { useFonts } from "expo-font";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { View } from "react-native";
 
// 1. Prevent the splash from auto-hiding.
//    Without this, React mounts and the splash disappears immediately.
SplashScreen.preventAutoHideAsync().catch(() => {
  // Safe to ignore in hot-reload or WebView contexts.
});
 
export default function RootLayout() {
  // 2. Register your custom fonts.
  const [fontsLoaded, fontError] = useFonts({
    "NotoSansJP-Regular": require("../assets/fonts/NotoSansJP-Regular.ttf"),
    "NotoSansJP-Bold": require("../assets/fonts/NotoSansJP-Bold.ttf"),
  });
 
  // 3. Hide the splash only when fonts are loaded OR errored.
  useEffect(() => {
    if (fontsLoaded || fontError) {
      SplashScreen.hideAsync();
    }
  }, [fontsLoaded, fontError]);
 
  // 4. Render nothing until fonts are ready — this is the key to no flicker.
  if (!fontsLoaded && !fontError) {
    return null;
  }
 
  return (
    <View style={{ flex: 1 }}>
      <Stack screenOptions={{ headerShown: false }} />
    </View>
  );
}

The critical bit is step 4: returning null until fonts are ready. By deferring the first render, we physically remove the window where the system font could show through. Including fontError in the guard means that even if a font file is broken, the app still moves past the splash instead of hanging forever.

What you should observe

  • Startup takes slightly longer than before (roughly font load + 100–300ms).
  • The moment the splash disappears, your first screen is already painted in the custom font.
  • If font loading fails, the splash still hides — you just fall back to system fonts gracefully.

Edge cases that have bitten me

1. Wrong require path

A typo like assets/font/ vs assets/fonts/ will crash Metro with Unable to resolve module. More commonly, the file name casing or hyphenation doesn't match — Google Fonts ships NotoSansJP-Regular.ttf, so match that exact string both in your useFonts key and in the file name.

2. Using the filename (with extension) as fontFamily

A common mistake is <Text style={{ fontFamily: "NotoSansJP-Regular.ttf" }}>. The correct value is the key you passed to useFonts, without the extension:

<Text style={{ fontFamily: "NotoSansJP-Regular", fontSize: 16 }}>
  Hello, custom-font world
</Text>

When you ask Rork to "render this text in Noto Sans JP Regular", it occasionally produces the extension-included version. A quick grep -n "\.ttf\"" app/ after generation catches this before it hits a device.

3. iOS caching an old font file

If you swap out a font during development and the simulator still shows the old one, the culprit is almost always a cache. In order:

  • Clear Metro: npx expo start --clear
  • Delete the app from the simulator's home screen and reinstall
  • If you're on a bare workflow, wipe ios/build and android/build and rebuild

4. Splash never hides

If useFonts stays stuck at [false, undefined], nine times out of ten it's a path resolution problem. expo-font raises an error internally, but your useEffect only reacts if fontError is set. Add temporary logging until you can reproduce it:

useEffect(() => {
  if (fontsLoaded) console.log("[Font] loaded OK");
  if (fontError) console.log("[Font] error:", fontError);
  if (fontsLoaded || fontError) {
    SplashScreen.hideAsync();
  }
}, [fontsLoaded, fontError]);

5. Fonts look correct in dev but wrong in production

If your dev build shows the right font but the EAS-built production binary shows the system font, check that your font files are included in the build. Specifically, confirm:

  • Font files are inside assets/fonts/ (not src/assets/fonts/ or anywhere custom).
  • Your app.json or app.config.js does not have an assetBundlePatterns entry that excludes the font folder.
  • You did not accidentally .gitignore the fonts, leaving them missing on the CI runner.

I once spent an afternoon debugging this only to realize my teammate had added assets/fonts/*.ttf to .gitignore after running a font-license check script. The CI build had no fonts at all. A simple git ls-files assets/fonts/ saved future me.

How to phrase this in a Rork prompt

When you want Rork to generate this setup in one shot, the prompt needs four facts:

  • Which font family and weights you want
  • Where you placed the font files
  • That you want expo-splash-screen to hold until fonts load
  • That the logic should live in _layout.tsx

Here's the template I keep in my notes:

I placed Noto Sans JP Regular and Bold in assets/fonts/. In app/_layout.tsx, load them with useFonts, prevent expo-splash-screen from auto-hiding, and only hide the splash once fonts are loaded. Return null before fonts are ready so we don't see a FOUT flash.

If the result isn't quite right, I've found it's more effective to add one small correction at a time ("also include fontError in the guard", "swallow the preventAutoHideAsync error") rather than rewriting the whole prompt. Small, iterative nudges give Rork the best context.

Cutting bundle size when your font is Japanese or CJK

Noto Sans JP is wonderful, but the full Regular file ships around 5 MB. Shipping two weights (Regular + Bold) adds ~10 MB to your app bundle. For an app that's otherwise only 20–30 MB, that's a meaningful hit on install size and first-launch download.

Two practical mitigations have worked for me:

Subset the font to the glyphs you actually use. If your app is primarily Japanese with a known vocabulary (say, a meditation app with a fixed set of phrases), tools like pyftsubset (part of fonttools) can cut the font file down to a tenth of its original size. You keep only the glyphs that appear in your app copy.

# Extract glyphs from your translation files into a unicode list,
# then run pyftsubset to create a trimmed font.
pip install fonttools
pyftsubset NotoSansJP-Regular.ttf \
  --unicodes-file=used_unicodes.txt \
  --output-file=NotoSansJP-Regular.subset.ttf

Swap the file in assets/fonts/ with the subset version and your app suddenly ships in a much smaller package. Just remember to re-run the subsetter whenever you add new user-visible strings.

Lazy-load the font for offscreen content. If your app has a rarely used screen (say, a legal notices page) that needs a different weight, you can fetch the font from your CDN at runtime rather than bundling it. expo-font supports loading from a URL via Font.loadAsync({ "FontName": { uri: "https://..." } }). This pattern costs you a brief loading state on the destination screen, but it keeps your initial bundle lean.

A few patterns I've settled on after shipping several apps

Over several Rork projects I've converged on a few small conventions that keep the font story boring and predictable:

  • Put every custom font in assets/fonts/ — no subfolders. This keeps require paths short and reviewable at a glance.
  • Use only two weights (Regular and Bold) unless the design genuinely calls for more. Every extra weight adds megabytes, and most mobile UIs look cleaner with fewer weights anyway.
  • Wrap every Text in an AppText from day one, even if the first version just forwards props. You'll thank yourself the first time you want to change the default font or color globally.
  • Keep the font loading logic in _layout.tsx only. I've seen projects scatter useFonts across several screens, and it always ends in tears when one screen forgets to guard rendering on fontsLoaded.

A small extra tip for Android

On iOS, Metro bundles your fonts reliably. On Android — especially on some Chinese-market devices running heavily modified ROMs — I've seen flaky fontFamily resolution. A defensive wrapper around Text solves it cleanly:

import { Text, TextProps } from "react-native";
 
export function AppText(props: TextProps) {
  return (
    <Text
      allowFontScaling={true}
      {...props}
      style={[
        { fontFamily: "NotoSansJP-Regular", color: "#222" },
        props.style,
      ]}
    />
  );
}

Using <AppText> everywhere instead of the raw <Text> means a future font swap becomes a one-line change.

What to try next

Open your current Rork project, paste the _layout.tsx snippet, and relaunch the simulator. The difference in feel is immediate — the app stops announcing itself as "an Expo app" and starts feeling like your app. From there, unifying typography across the whole app is what turns Rork's "good enough" UI into something you're actually proud to ship.

If you want to keep leveling up the polish of Rork-generated UIs, I've written about related tactics in Fixing the "cheap look" of Rork-generated UIs and Animations in Rork apps.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.
Dev Tools2026-08-10
Switching to Signed URLs Killed My Image Cache — Decoupling expo-image Keys from the URL
Signed URLs rewrite their query string on every expiry, so a URL-keyed cache never hits. Here is how to derive a stable key and drive expo-image's writeToCacheAsync and readFromCacheAsync yourself, with measured results.
Dev Tools2026-08-01
Every Experiment Kept Landing on the Same Devices: Hash Choice and Salt Position, Measured Across a Million IDs
On-device experiment assignment looked fine in isolation, then collapsed the moment two experiments ran side by side. Here is what one million IDs revealed about four hash functions, why the culprit was the concatenation order rather than hash quality, and the implementation I settled on, with the measured numbers.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →