Created
April 23, 2025 22:23
-
-
Save EvanBacon/0e9b181e64d15a1d005ff61e5c34a5ee to your computer and use it in GitHub Desktop.
Loading fonts with React Suspense in Expo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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