webfontloader: The WebFontLoader breaks when imported in a server side rendered app

I have a React app running with SSR.

When I import WebFontLoader from 'webfontloader' the SSR crashes with window is not defined.

Would it make sense to add a check for typeof window !== 'undefined' in the package?

If not I would have to fork and maintain such change. Thanks

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 38
  • Comments: 18

Most upvoted comments

In Next js. This is worked well


  useEffect(() => {
    (async () => {
      const WebFont = await import('webfontloader');
      WebFont.load({
        custom: {
          families: ['BlackHanSans', 'BMEuljiro10yearslater', 'Cafe24Danjunghae', 'DOSMyungjo', 'HandLetter'],
        },
      });
    })();
  }, []);

Yep totally relevant; I was looking to load web fonts in a Frontity project and ran into this error today. My solution was a modified version of @neaumusic with hooks in case someone wants to just copy and paste it:

  useEffect(() => {
    const WebFontLoader = require('webfontloader')
    WebFontLoader.load({
      google: {
        families: ['Ubuntu', 'Special Elite']
      }
    })
  }, [])

Although it would be great if this could be handled in the package itself since a big use-case would be SSR apps.

Please add window check! Impossible to use for events listening without ugly hacking.

Here is my approach

export default function () {
  if (sessionStorage.getItem('fonts')) {
    // If the font had been loaded before
    // Just active the font immediately
    document.documentElement.classList.add('wf-active')
  }

  // We use dynamic import to prevent "window is not defined" error
  import('webfontloader').then(WebFontLoader => {
    WebFontLoader.load({
      timeout: 3000,
      typekit: {
        id: KIT_ID,
      },
      active: () => {
        sessionStorage.setItem('fonts', true)
      },
    })
  })
}

I’m dynamically importing the webfontloader after checking if it’s in the browser:

if (typeof window !== 'undefined') {
  const WebFont = require('webfontloader');
  ...
}

Issue opened for 2y+ without official feedback, the solution is simple though.

@bramstein Pinging you for official feedback on this 😃

Workarounds exist, but it slows down adoption of this package for universal apps, such as when using https://github.com/zeit/next.js


Personal solution:

import { isBrowser } from '@unly/utils';
if (isBrowser()) {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const WebFontLoader = require('webfontloader');

    // Load our fonts. Until they're loaded, their fallback fonts will be used
    // This fixed an issue when loading fonts from external sources that don't show the text until the font is loaded
    // With this, instead of not showing any text, it'll show the text using its fallback font, and then show the font once loaded
    WebFontLoader.load({
      typekit: {
        id: 'dddddd', // Load "neuzeit-grotesk" font
      },
    });
  }

For instance, I’ve used it in my src/components/Head.tsx:

import { isBrowser } from '@unly/utils';
import NextHead from 'next/head';
import React from 'react';

/**
 * Custom Head component
 *
 * https://github.com/zeit/next.js#populating-head
 *
 * @param title
 * @param description
 * @param ogImage
 * @param url
 * @param favicon
 * @param lang
 * @constructor
 */
const Head: React.FunctionComponent<Props> = (
  {
    title,
    description,
    ogImage,
    url,
    favicon,
    additionalContent,
  },
): JSX.Element => {
  if (isBrowser()) {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const WebFontLoader = require('webfontloader');

    // Load our fonts. Until they're loaded, their fallback fonts will be used
    // This fixed an issue when loading fonts from external sources that don't show the text until the font is loaded (FOUT - See https://www.npmjs.com/package/webfontloader#get-started)
    // With this, instead of not showing any text, it'll show the text using its fallback font, and then show the font once loaded
    WebFontLoader.load({
      typekit: {
        id: 'dddd', // Load "neuzeit-grotesk" font
      },
    });
  }

  return (
    <NextHead>
      <meta charSet="UTF-8" />
      <title>{title || defaultTitle}</title>
      <meta
        name="description"
        content={description || defaultDescription}
      />
      <meta name="viewport" content="width=device-width, initial-scale=1" />
      <link rel="icon" sizes="192x192" href="/touch-icon.png" />
      <link rel="apple-touch-icon" href="/touch-icon.png" />
      <link rel="mask-icon" href="/favicon-mask.svg" color="#49B882" />
      <link rel="icon" href={favicon || defaultFavicon} />
      <link rel="stylesheet" href="static/fonts/CircularStd-Book/font.css" />

      <meta property="og:url" content={url || defaultOGURL} />
      <meta property="og:title" content={title || ''} />
      <meta
        property="og:description"
        content={description || defaultDescription}
      />
      <meta name="twitter:site" content={url || defaultOGURL} />
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:image" content={ogImage || defaultOGImage} />
      <meta property="og:image" content={ogImage || defaultOGImage} />
      <meta property="og:image:width" content="1200" />
      <meta property="og:image:height" content="630" />

      {/* Detect outdated browser and display a popup about how to upgrade to a more recent browser/version */}
      {/* XXX See public/static/CDN/README.md */}
      <script async={true} src="https://storage.googleapis.com/the-funding-place/assets/libs/outdated-browser-rework/outdated-browser-rework.min.js" />
      <link rel="stylesheet" href="https://storage.googleapis.com/the-funding-place/assets/libs/outdated-browser-rework/outdated-browser-rework.css" />

      {
        additionalContent && (
          additionalContent
        )
      }

    </NextHead>
  );
};

type Props = {
  title?: string;
  description?: string;
  url?: string;
  ogImage?: string;
  favicon?: string;
  additionalContent?: React.ReactElement;
}

export default Head;

Then, I import Head from both my _document.tsx and other pages/*.tsx files.

_document.tsx

// ...
render(): JSX.Element {
    const { lang }: DocumentProps & { lang: string } = this.props;

    return (
      <html lang={this.props.lang}>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }

it’s purely import that’s broken, just require inline

componentDidMount() {
  const WebFontLoader = require('webfontloader');
}

@TrejoCode Updated previous answer to provide more information 😃

one option if you are using webpack for transpiling your server side, you can add a null-loader as an alias for webfontloader

  resolve: {
    alias: {
      'webfontloader': 'null-loader',
    },
  },