Within Next JS, you're forced to architect your React components in the most optimized way. Otherwise you'll run into slight delays, and undesirable side effects.
In this walkthrough, I'll show one common, and undesirable side effect when using client within Next JS.
The Problem
Notice how you see the content / text first before you see the gsap animation kick in ?
At first, maybe i thought this was a side effect using GSAP. But no, it was stemming from somewhere else....
Let's look at the code for this.
LandingPage.tsx
'use client';
import Hero from '../common/Hero/Hero';
function LandingPage() {
return <Hero />;
}
export default LandingPage;
Hero.tsx
'use client';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useEffect, useLayoutEffect, useRef } from 'react';
import './style.css';
function Hero() {
const introText = useRef(null);
const secondIntroText = useRef(null);
const finalButton = useRef(null);
useLayoutEffect(() => {
const tl = gsap.timeline();
gsap.registerPlugin(ScrollTrigger);
tl.to(introText.current, {
duration: 0.5,
autoAlpha: 1,
ease: 'easeIn-Out',
delay: 0.5,
x: -100,
})
.to(secondIntroText.current, {
duration: 0.5,
autoAlpha: 1,
ease: 'easeOut',
delay: 0.2,
x: 10,
y: -100,
})
.to(finalButton.current, {
duration: 0.5,
autoAlpha: 1,
ease: 'easeOut',
delay: 0.1,
y: 0,
x: 0,
});
}, []);
return (
<>
<div className='lg:2/6 xl:w-2/4 mt-20 lg:mt-40 lg:ml-16 text-left'>
<div ref={introText} className='text-6xl font-semibold text-gray-900 leading-none intro-text'>
Bring all your work together
</div>
<div ref={secondIntroText} className='mt-6 text-xl font-light text-true-gray-500 antialiased second-text'>
A better experience for yout attendees and less stress yout team.
</div>
<button
ref={finalButton}
className='mt-6 px-8 py-4 rounded-full font-normal
tracking-wide bg-gradient-to-b
text-black outline-none focus:outline-none hover:shadow-lg
bg-third transition duration-200 ease-in-out final-button'>
Join Today
</button>
</div>
<div className='mt-12 lg:mt-32 lg:ml-20 text-left'></div>
</>
);
}
export default Hero;
Used in Next JS SSR
"use client";
import dynamic from "next/dynamic";
import React from "react";
import Footer from "../src/common/Footer";
const LandingPage = dynamic(() => import("../src/Pages/LandingPage"), {
ssr: true,
});
// export async function generateStaticParams() {
// const posts = await fetch('https://.../posts').then((res) => res.json());
// return posts.map((post: any) => ({
// slug: post.slug,
// }));
// }
export default function Bootstrap() {
return (
<>
<LandingPage />
<Footer />
</>
);
}
Maybe if i wrapped the Hero
component in a react memo, it will resolve it ? cause we are trying to keep things optimized.
Nope.. not in this case.
The solution
When it comes to delays, and such, what i've noticed is when you don't compose your react components in a performant manner, it will result in undesirable behavior.
Let's go back at the code
"use client";
import dynamic from "next/dynamic";
import React from "react";
import Footer from "../src/common/Footer";
const LandingPage = dynamic(() => import("../src/Pages/LandingPage"), {
ssr: true,
});
// export async function generateStaticParams() {
// const posts = await fetch('https://.../posts').then((res) => res.json());
// return posts.map((post: any) => ({
// slug: post.slug,
// }));
// }
export default function Bootstrap() {
return (
<>
<LandingPage />
<Footer />
</>
);
}
LandingPage
is essentially an ssr / client component. It really is just a client side component.
Within LandingPage
we have to import the Hero
component. Although this makes sense to most react developers, this approach will lead to component delays, as you seen in the problem statement.
Also, our landing page might grow, we will have more components being used within this component, and their components will have dependencies that will be quite expensive to load effiencetly.
E.g
Imagine if a component you're using inside your landing page component is using a dependency that is large in size, you can imagine next js will delay the load of this component.
How do we solve this ?
You solve this issue by effective component composition.
Nadia Makarevich has a detailed post about composition in React.
In our case, this problem is super easy to resolve. We just make our LandingPage
component take in children
as the prop.
Like this
'use client';
import { ReactNode } from 'react';
function LandingPage({ children }: { children: ReactNode }) {
return <>{children}</>;
}
export default LandingPage;
Then in our Next JS file.
bootstrap.tsx (this file gets called on page.tsx by the way)
"use client";
import dynamic from "next/dynamic";
import React from "react";
import Footer from "../src/common/Footer";
import Hero from "../src/common/Hero/Hero";
const LandingPage = dynamic(() => import("../src/Pages/LandingPage"), {
ssr: true,
});
// export async function generateStaticParams() {
// const posts = await fetch('https://.../posts').then((res) => res.json());
// return posts.map((post: any) => ({
// slug: post.slug,
// }));
// }
export default function Bootstrap() {
return (
<>
<LandingPage> // landing page takes react components as children.
<Hero />.
</LandingPage>
<Footer />
</>
);
}
This will load our components more efficiently.
Now lets look at the result..
The animation works instantly without showing the component contents first, and then the.... animation second.
The animation works out the gate, without any weird side effects or delays.
Side note
You don't need to do this for all your client side components. You have to gauge which components are having the above mentioned side effects, before committing to make these changes.
If your components are working fine, no delays, or weird side effects, no need to change it. This is just a way to resolve said issue.
Conclusion
Hope this helps, cheers 🥳 🥳.
Top comments (0)