Mar 21, 2020

Browser-Only React Components With Suspense

When building your Gatsby site, every page of it is prerendered completely to be shipped to your browser and displayed blazingly fast, compared to parsing and setting up the JavaScript code required to render the React components first, as done in traditional SPAs (e.g. create-react-app). The Gatsby team released an awesome, in-depth post about the reasons for doing this.

One natural problem with this approach is, that it gets hard to pre-render content that depends on the user, for example, components depending on your viewport to determine which width and height to use. Another issue happens when you try to pre-render components that change a lot, if the structure that gets returned during build time change on hydration, you'll be in for a chaotic ride.

Luckily, Gatsby, or rather React Suspense, has us covered in this case, too! There are a few ways to get client-side-only code running with Gatsby, as documented here, but I'll be showcasing the React Suspense way!

const ThisOnlyWorksInABrowser = () => {
  // Do some browser-specific logic here
  return <p>This will not be pre-rendered</p>;
};

// This is important! We need to use a default
// export for the dynamic import to work!
export default ThisOnlyWorksInABrowser;

Our component above doesn't do much, but it's an example of a piece of code that will only work in a browser environment. To get this imported later, we need to use a default export, not a named one.

A great post on understanding pre-rendering and mitigating issues arising from Gatsby's SSR flow inspired me to use the mounting check logic to isolate browser-only code.

import React from 'react';

// Lazy-load client-side-only component
const ClientSideOnlyLazy = React.lazy(() =>
  import('./PathToMyBrowserOnlyComponent')
);

const ClientSideOnlyComponent = () => {
  const [hasMounted, setHasMounted] = React.useState(false);

  React.useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) {
    return null;
  }

  // We can render our browser-only component
  // lazily using Suspense and a dynamic import
  return (
    <React.Suspense fallback={<div />}>
      <ClientSideOnlyLazy />
    </React.Suspense>
  );
};

const MyPage = () => (
  <>
    <Header />
    <Navigation />
    <ClientSideOnlyComponent />
  </>
);

We've added quite a few parts now: We're essentially creating a lazy-loadable version of our browser-only component using a dynamic import and React's lazy feature, then rendering this component wrapped in a Suspense component, guarded by our hasMounted condition that makes sure we're not trying to render this component while in SSR mode.

That's already everything you'll need to make your browser-only components working without SSR interfering! Please note, that this is kind of an escape-hatch functionality provided by the build tooling, in most cases you'll want to make your components SSR-compatible to harness the performance and SEO benefits you would be missing out on otherwise.


Thanks for reading this short guide! If you've got any questions, suggestions, or feedback in general, don't hesitate to reach out on Twitter or by mail.