expo/google-fonts

Little hack that I made

SrBrahma opened this issue ยท 6 comments

Hey there. I just made this little code so I can keep all my fonts in a single file, and easily access and use them with Intellisense. I couldn't find a way to avoid the duplicate writing of the font in import and in the useFontsArg (as this lib also requires). However, still a nice hack!

// A 'fonts.ts' file
import {
  NotoSans_400Regular,
  NotoSans_700Bold
} from '@expo-google-fonts/noto-sans';

import {
  Roboto_400Regular,
  Roboto_500Medium,
  Roboto_700Bold
} from '@expo-google-fonts/roboto';

export { useFonts } from 'expo-font';

export const useFontsArg = {
  NotoSans_400Regular,
  NotoSans_700Bold,
  Roboto_400Regular,
  Roboto_500Medium,
  Roboto_700Bold
};


type PropsToString<Obj> = {
  [K in keyof Obj]: string
}

export const fonts = { ...useFontsArg } as unknown as PropsToString<typeof useFontsArg>;
Object.keys(fonts).forEach((e: any) => (fonts as any)[e] = e );
// App.tsx
export default function App() {
  const [fontsLoaded] = useFonts(useFontsArg);

  if (!fontsLoaded)
    return <AppLoading/>;

  return <Text style={{fonts.Roboto_500Medium}}> Hey! </Text>` // Intellisense guides you! Yay!
}

Thanks so much for sharing, @SrBrahma! I'm already using it ๐Ÿ’ช

The styles prop should be something like this: style={{ fontFamily: fonts.Roboto_500Medium }} though ๐Ÿ‘€

Thanks so much for sharing, @SrBrahma! I'm already using it ๐Ÿ’ช

The styles prop should be something like this: style={{ fontFamily: fonts.Roboto_500Medium }} though ๐Ÿ‘€

Fixed!

Thanks!

I've done an update right now as I am starting another project and I like to keep stuff smart ;)

It also allows to wait for Icons to be loaded.

fonts.ts
// https://docs.expo.dev/guides/using-custom-fonts/
// I first shared it in here: https://github.com/expo/google-fonts/issues/6
import { Platform } from 'react-native';
import {
  MaterialCommunityIcons // Commonly used
} from '@expo/vector-icons';
// import * as DM_Sans from '@expo-google-fonts/dm-sans';
// import * as Inter from '@expo-google-fonts/inter';
import * as Roboto from '@expo-google-fonts/roboto'; // Aditional fontWeights and to be used on iOS
import { useFonts } from 'expo-font';


// Instead of using the useFonts(fontsArg) hook to get the loaded state, use useMyFonts().

/** Enter here your fonts to be loaded.
 * 
 * You may enter in the object `...Roboto` for example to load the whole family,
 * or `Roboto.Roboto_500Medium` to only load the specific font. */
const fontsToLoad = {
  ...Roboto,
};

// Enter here your icons to be used and loaded/cached on start.
/** Use the icons via `<Icons.MaterialCommunityIcons/>` */
export const Icons = {
  MaterialCommunityIcons,
};

// Manual system fonts aliases to be added to F.
const additional = {
  monospace: Platform.OS === 'ios' ? 'Courier' : 'monospace',
};


/** Fonts. `F` instead of `Fonts` as it's commonly used.
 * 
 * In your component style, use `{ fontFamily: F.Roboto_500Medium }`.
 * 
 * It's type-smart! Intellisense will autofill and TS will complain if it's wrong */
export const F = getFont()



// === Implementation ===

const iconsFonts = (Object.fromEntries(Object.entries(Icons).map(([iconName, icon]) => ([iconName, icon.font]))));

const useFontsArg = {
  ...(fontsToLoad as Omit<typeof fontsToLoad, '__metadata__' | 'useFonts'>),
  ...iconsFonts,
};

// Remove not-font stuff
delete (useFontsArg as any).__metadata__;
delete (useFontsArg as any).useFonts;

type PropsToString<Obj> = { [K in keyof Obj]: string };

// Prettify obj type
type Id<T> = unknown & { [P in keyof T]: T[P] };


function getFont() {
  return {
    ...Object.fromEntries(Object.keys(useFontsArg).map((key) => [key, key])),
    ...additional,
  } as Id<PropsToString<typeof useFontsArg> & typeof additional>;
}

export function useMyFonts(): [fontsLoaded: boolean, error: Error | null] {
  return useFonts(useFontsArg);
}

Maybe I can make it into a npm package, if there is demand or I want to better use it on my projects.

It could be a single function, like

~ function getMyFonts(p: {icons, fonts, additional}): { useMyFonts, Icons, I, Fonts, F }

The above had some errors. I made the function I said. It's used like the following:

import { Platform } from 'react-native';
import { Entypo, MaterialCommunityIcons } from '@expo/vector-icons';
import * as Roboto from '@expo-google-fonts/roboto';

export const { F, Icons, useFonts } = createFontsToLoad({
  fontsToLoad: {
    ...Roboto,
  },
  iconsToLoad: {
    MaterialCommunityIcons,
    Entypo,
  },
  aliasesToSystemFonts: {
    monospace: Platform.OS === 'ios' ? 'Courier' : 'monospace',
  },
});

// On App init
const [loaded, error] = useFonts();

// Font usage
const styles = StyleSheet.create({
  text: {
    fontFamily: F.Roboto_500Medium // Type safe and smart! And loaded!
  },
  monoText: {
    fontFamily: F.monospace
  }
});

// Icon usage. Loaded and quick to switch to other icons!
const component = <Icons.MaterialCommunityIcons ... />
Source
// https://docs.expo.dev/guides/using-custom-fonts/
// I first shared it in here: https://github.com/expo/google-fonts/issues/6
// Aditional fontWeights and to be used on iOS
import { useFonts as expoUseFonts } from 'expo-font';



/** Prettify obj type */
type Id<T> = unknown & { [P in keyof T]: T[P] };

type FontsToLoad = Id<{
  useFonts?: any;
  __metadata__?: any;
  // [x in string]: any
}>;


type SystemAliases = Record<string, string>;
// Simplified version, so we don't need `@expo/vector-icons` pkg to `import { Icon } from '@expo/vector-icons/build/createIconSet'`.
type Icon = {font: Record<string, any>};
type Icons = Record<string, Icon>;

type OmitFontsMeta<F extends FontsToLoad> = Omit<F, '__metadata__' | 'useFonts'>;
/** Omits meta props and converts the other FontsToLoad props to string type. */
type FontsToLoadToFonts<F extends FontsToLoad> = {[K in keyof OmitFontsMeta<F>]: string};
/** @defaul */
type Fonts<F extends FontsToLoad, A extends SystemAliases> = Id<FontsToLoadToFonts<F> & A>;




type FontsToLoadProps<F extends FontsToLoad, I extends Icons, A extends SystemAliases> = {
  /** The fonts to be loaded.
   *
   * @example
   * ```ts
   * // Load whole family
   * import * as Roboto from '@expo-google-fonts/roboto'
   * // Load single font
   * import { Inter_900Black } from '@expo-google-fonts/inter';
   *
   * const { F, useFonts } = createFontsToLoad({
   *   fontsToLoad: {
   *     ...Roboto,
   *     Inter_900Black
   *   }
   * })
   * ```
   * */
  fontsToLoad: F;
  /** Your icons to be loaded/cached on useFonts.
   *
   * @example
   * ```ts
   * import { Entypo, MaterialCommunityIcons } from '@expo/vector-icons';
   *
   * const { I, useFonts } = createFontsToLoad({
   *   iconsToLoad: {
   *     Entypo
   *     MaterialCommunityIcons
   *   }
   * })
   * ```
  */
  iconsToLoad: I;
  /** Manual system fonts aliases to be added to Fonts.
   *
   * System fonts don't need to be loaded, this only adds the alias to Fonts.
   *
   * @example
   * ```ts
   * const { F } = createFontsToLoad({
   *   aliasesToSystemFonts: {
   *     monospace: Platform.OS === 'ios' ? 'Courier' : 'monospace',
   *   }
   * })
   * ```
   *
   *
  */
  aliasesToSystemFonts: A;
};

type FontsToLoadRtn<F extends FontsToLoad, I extends Icons, A extends SystemAliases> = {
  /** Short alias to `Fonts`.
   *
   * To be used in a style like `{fontFamily: F.Roboto_500Medium }` */
   F: Fonts<F, A>;
   /** To be used in a style like `{fontFamily: Fonts.Roboto_500Medium }` */
   Fonts: Fonts<F, A>;
   /** To be used just like the default useFonts(args), without the args. */
   useFonts: () => [fontsLoaded: boolean, error: Error | null];
   /** Short alias to `Icons`.
    *
    * Icons to be used via `<Icons.MaterialCommunityIcons/>`. */
   I: I;
   /** Icons to be used via `<Icons.MaterialCommunityIcons/>`. */
   Icons: I;
};

/** Instead of using the useFonts(fontsArg) hook to get the loaded state, use useMyFonts() on your App start. */
export function createFontsToLoad<F extends FontsToLoad, I extends Icons, A extends SystemAliases>({
  fontsToLoad,
  iconsToLoad,
  aliasesToSystemFonts,
}: FontsToLoadProps<F, I, A>): FontsToLoadRtn<F, I, A> {

  const fontsToLoadInternal = { ...fontsToLoad } as Id<Omit<typeof fontsToLoad, '__metadata__' | 'useFonts'>>;
  // Remove non-font stuff
  delete (fontsToLoadInternal as any).__metadata__;
  delete (fontsToLoadInternal as any).useFonts;

  const iconsFonts = Object.values(iconsToLoad)
    .reduce((obj, icon) => ({ ...obj, ...icon.font }), {} as Record<string, number>);

  const useFontsArg = {
    ...fontsToLoadInternal,
    ...iconsFonts,
  };

  const Fonts = {
    ...Object.fromEntries(Object.keys(fontsToLoadInternal).map((key) => [key, key])),
    ...aliasesToSystemFonts,
  } as Fonts<F, A>;

  const useFonts = () => expoUseFonts(useFontsArg);

  return {
    F: Fonts,
    Fonts,
    useFonts,
    I: iconsToLoad,
    Icons: iconsToLoad,
  };
}

I am busy right now but eventually will turn it into a npm package. Or, I think it could be added to this lib or any other related Expo lib. Edit: It has been released! https://github.com/SrBrahma/expo-font-loader

how you use this for local font in assets?