Makuhari Development Corporation
5 min read, 921 words, last updated: 2023/12/27
TwitterLinkedInFacebookEmail

Try Vanta.js with React Nextjs SSR

Are you still using compressed video for your web site background?

Nowadays, we could use Three.js to make fancy animations. Sounds quite heavy but actually, there are already some existing examples been summarized, like vanta.js.

It's kind of outdated but let's check out if we can use it in our React + Nextjs project.

See what we got there

According to the description, we can simply involve the packaged scripts and that's all.

<script src="three.r134.min.js"></script>
<script src="vanta.clouds.min.js"></script>
<script>
VANTA.CLOUDS({
  el: "#your-element-selector",
  mouseControls: true,
  touchControls: true,
  gyroControls: false,
  minHeight: 200.00,
  minWidth: 200.00
})
</script>

https://codesandbox.io/s/tyuss

It works well on CodeSandbox, so let's move on to use it inside our JSX components in Nextjs.

We don't really want to install anything

Following the docs, we can use a CSR component with hooks to set config after mounted.

But it needs us to

  • install the vanta dependency
  • optionally declare the module by ourself, or just @ts-ignore, since currently there is no TS support
  • write useState, useEffect, useRef a lot ...
'use client';
 
import { useState, useEffect, useRef } from 'react';
import Script from 'next/script';
import CLOUDS from 'vanta/dist/vanta.clouds.min';
 
export default function Default() {
  const [vantaEffect, setVantaEffect] = useState(null);
  const myRef = useRef(null);
 
  useEffect(() => {
    if (!vantaEffect) {
      setVantaEffect(
        CLOUDS({
          el: myRef.current,
          mouseControls: true,
          touchControls: true,
          gyroControls: false,
          minHeight: 200.0,
          minWidth: 200.0,
        }),
      );
    }
  }, [vantaEffect]);
  return (
    <>
      <Script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js" />
      <Script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js" />
      <div ref={myRef}>Foreground content goes here</div>
    </>
  );
}

And still, if we arrange the scripts like above, we could get an error as below, and the animation just won't show up.

Uncaught TypeError: Cannot read properties of undefined (reading 'Color')

We expect a workable way for SSR component as simple as just importing the scripts and doing no more.

Some Approaches

Try to use scripts inside dangerouslySetInnerHTML

by just inject the scripts within a JSX div as below

<div
  id="homepage-background"
  dangerouslySetInnerHTML={{
    __html: `
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js"></script>
    <script>
      VANTA.CLOUDS({
        el: "#homepage-background",
        mouseControls: true,
        touchControls: true,
        gyroControls: false,
        minHeight: 200.00,
        minWidth: 200.00
      });
    </script>
  `,
  }}
></div>

Great, the animation is rendered, but we are having a hydration error

Warning: Prop dangerouslySetInnerHTML did not match.
Server: ... /script>

and the actual DOM structure looks like this

<div class="flex min-h-screen flex-col justify-end" id="homepage-background" style="position: relative;">
  <span style="position: relative; z-index: 1;"></span>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js" style="position: relative; z-index: 1;"></script>
  <span style="position: relative; z-index: 1;"></span>
  <script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js" style="position: relative; z-index: 1;"></script>
  <span style="position: relative; z-index: 1;"></span>
  <script style="position: relative; z-index: 1;">
    VANTA.CLOUDS({
      el: "#homepage-background",
      mouseControls: true,
      touchControls: true,
      gyroControls: false,
      minHeight: 200.00,
      minWidth: 200.00
    });
  </script>
  <canvas class="vanta-canvas" width="960" height="292" style="position: absolute; z-index: 0; top: 0px; left: 0px; width: 1440px; height: 438px;"></canvas>
</div>

seems not exactly what we want, well it passes the build and the error only shows up in development environment.

So, technically it's good to go, but could cause issues later on. Let's try a little more.

Try to render after mounted

This won't solve the hydration issue though.

'use client';
 
import { useState, useEffect } from 'react';
 
export default function Default() {
  const [isMounted, setIsMounted] = useState(false);
  useEffect(() => {
    setIsMounted(true);
  }, []);
  return (
    isMounted && (
      <div
        className="flex min-h-screen flex-col justify-end"
        id="homepage-background"
        dangerouslySetInnerHTML={{
          ...
        }}
      ></div>
    )
  );
}

Try the nextjs dynamic import

This won't work either.

const DynamicComponent = dynamic(() => import('./@background/default'), {
  loading: () => <p>Loading...</p>,
  ssr: false,
});
export default function Default() {
  return (
    <div
      className="flex min-h-screen flex-col justify-end"
      id="homepage-background"
      dangerouslySetInnerHTML={{
        ...
      }}
    ></div>
  );
}

Try next/script got VANTA not defined error

If we use the next/script directly, we just faced an error

Uncaught ReferenceError: VANTA is not defined

import Script from 'next/script';
 
return (
  <div>
    <div id="homepage-background"></div>
    <Script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js" />
    <Script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js" />
    <Script id="script">
      {`VANTA.CLOUDS({
        el: "#homepage-background",
        mouseControls: true,
        touchControls: true,
        gyroControls: false,
        minHeight: 200.00,
        minWidth: 200.00
      });`}
    </Script>
  </div>
)

Why? Inside normal HTML body, this just worked fine, what's wrong with Nextjs?

The Solution

Use next/script with correct strategy

If we look close to the next/script docs, we can find out that there is a paragraph explaining the script load strategy.

  • beforeInteractive: Load the script before any Next.js code and before any page hydration occurs.
  • afterInteractive: (default) Load the script early but after some hydration on the page occurs.
  • lazyOnload: Load the script later during browser idle time.
  • worker: (experimental) Load the script in a web worker.

Since afterInteractive is the default option, we changed to beforeInteractive and it just works like a charm.

import Script from 'next/script';
 
return (
  <div>
    <div id="homepage-background"></div>
    <Script
      src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"
      strategy="beforeInteractive"
    />
    <Script
      src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.clouds.min.js"
      strategy="beforeInteractive"
    />
    <Script id="script">
      {`VANTA.CLOUDS({
      el: "#homepage-background",
      mouseControls: true,
      touchControls: true,
      gyroControls: false,
      minHeight: 200.00,
      minWidth: 200.00
    });`}
    </Script>
  </div>
)    

Now we are using our fancy animation supported by remote third-party scripts, with customized configs, in our SSR component with React and Nextjs.

Final words

Keypoints:

  • use next/script to cache the remote scripts
  • if there are variables been used which are exported by the remote scripts, unlike plant HTML, you may need to adjust the load strategy inside Nextjs to make sure it works.

Further issues:

  • the animation won't persist when routing cross pages
  • protential performance issue after routing, could be improved by adjusting the screen size like open the browser development tool when facing it, reason remain unclear.
Makuhari Development Corporation
法人番号: 6040001134259
サイトマップ
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.