one
Version:
One is a React framework for web, iOS, and Android apps.
95 lines (84 loc) • 2.53 kB
text/typescript
import { useEffect, useState } from 'react'
import { Image } from 'react-native'
import { NitroModules } from 'react-native-nitro-modules'
import { rethrowNativeError } from '../nativeError'
import type { OneFonts } from '../specs/OneFonts.nitro'
import type { FontMap, Fonts as FontsApi, FontSource, UseFontsResult } from './types'
export type * from './types'
// the OneFonts nitro hybrid object is created once and lazily. null outside
// a native build.
let hybrid: OneFonts | null | undefined
function getModule(): OneFonts | null {
if (hybrid === undefined) {
hybrid = NitroModules.hasHybridObject('OneFonts')
? NitroModules.createHybridObject<OneFonts>('OneFonts')
: null
}
return hybrid
}
function resolveFontUri(name: string, source: FontSource): string {
if (typeof source === 'string') {
return source
}
const resolved = Image.resolveAssetSource(source)
if (!resolved) {
throw new Error(`Fonts.load: "${name}" is not a font asset`)
}
return resolved.uri
}
async function load(fonts: FontMap): Promise<void> {
const module = getModule()
if (!module) {
throw new Error('fonts need a native build')
}
const entries = Object.entries(fonts).map(([name, source]) => ({
name,
uri: resolveFontUri(name, source),
}))
await Promise.all(
entries.map(({ name, uri }) => module.load(name, uri).catch(rethrowNativeError))
)
}
function isLoaded(name: string): boolean {
const module = getModule()
if (!module) {
return false
}
return module.isLoaded(name)
}
export const Fonts: FontsApi = {
load,
isLoaded,
}
export function useFonts(fonts: FontMap): UseFontsResult {
// seeded from the platform so a map that is already usable never
// flashes unloaded. the effect runs once on mount, like expo-font: a
// literal map is a new object every render, so [fonts] would reload
// forever.
const [loaded, setLoaded] = useState(() => Object.keys(fonts).every(isLoaded))
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
if (loaded) {
return
}
let cancelled = false
setError(null)
load(fonts).then(
() => {
if (!cancelled) {
setLoaded(true)
}
},
(reason) => {
if (!cancelled) {
setError(reason instanceof Error ? reason : new Error(String(reason)))
}
}
)
return () => {
cancelled = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return [loaded, error]
}