Skip to content

Instantly share code, notes, and snippets.

@EvanBacon
Created April 23, 2025 22:23
Show Gist options
  • Save EvanBacon/0e9b181e64d15a1d005ff61e5c34a5ee to your computer and use it in GitHub Desktop.
Save EvanBacon/0e9b181e64d15a1d005ff61e5c34a5ee to your computer and use it in GitHub Desktop.
Loading fonts with React Suspense in Expo
import { SplashScreen } from "expo-router";
import React, { Suspense, useEffect } from "react";
import { Text, View } from "react-native";
import * as Font from "expo-font";
SplashScreen.preventAutoHideAsync();
function SplashFallback() {
useEffect(
() => () => {
SplashScreen.hideAsync();
},
[]
);
return null;
}
export default function RootLayout() {
return (
<Suspense fallback={<SplashFallback />}>
<AsyncFont
src={require("@/assets/fonts/SpaceMono-Regular.ttf")}
fontFamily="SpaceMono"
/>
<View
style={{
flex: 1,
gap: 64,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 24 }}>Before</Text>
<Text style={{ fontFamily: "SpaceMono", fontSize: 24 }}>After</Text>
</View>
</Suspense>
);
}
function wrapPromise<T>(promise: Promise<T>) {
let status: "pending" | "success" | "error" = "pending";
let result: T | unknown;
let suspender = promise.then(
(r: T) => {
console.log("font loaded", r);
status = "success";
result = r;
},
(e: unknown) => {
console.log("font error", e);
status = "error";
result = e;
}
);
return {
read(): T {
if (status === "pending") {
console.log("font pending");
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result as T;
}
throw new Error("Unexpected state");
},
};
}
const fontMap = new Map();
const getSuspendingFont = (fontFamily: string, src: Font.FontSource) => {
const id = JSON.stringify(fontFamily + "|" + JSON.stringify(src));
if (!fontMap.has(id)) {
const loader = wrapPromise(Font.loadAsync({ [fontFamily]: src }));
fontMap.set(id, loader);
return loader.read();
}
return fontMap.get(id).read();
};
const getLoadedFont = React.cache(getSuspendingFont);
type FontProps = {
src: number | string;
fontFamily: string;
display?: Font.FontDisplay;
};
const AsyncFont = process.env.EXPO_OS === "web" ? CSSFont : SuspendingFont;
function SuspendingFont({ src, fontFamily }: FontProps) {
getLoadedFont(fontFamily, src);
return null;
}
function CSSFont({ src, fontFamily, display }: FontProps) {
// This font depends on CSS for the state updating.
// @ts-ignore
Font.useFonts({
[fontFamily]: { default: src, fontDisplay: display },
});
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment