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.