Jump to content
Search Community

How to recalculate smooth-scroll height using scrollTrigger in react route?

bs.choi test
Moderator Tag

Recommended Posts

Hi.

First of all, I apologize for my poor English using a translator.

 

I am making a project using next.js.

I implemented smooth-scroll using scrollTrigger while looking at the example in the official documentation.

 

It was working fine, but an unexpected problem occurred when the subpage was added.

// set(resize) height
function refreshHeight() {
    const headerH = document.getElementById("header_wrap").offsetHeight;
    height = scrollContent.clientHeight + headerH;
    scrollHeightBox.style.height = height + "px";
}
ScrollTrigger.addEventListener("refreshInit", refreshHeight);

As set in the code above, the height of the scroll-wrapper (div) remains the same as the height set in the main page even after moving to the subpage.

 

So I added the code below to fix this.

const routeBtn = document.querySelectorAll(".route");
routeBtn.forEach((elem) => {
    elem.addEventListener("click", ScrollTrigger.refresh);
});

As a result, it was confirmed that there is a slight delay when moving the page, but the height is reset properly.

 

But this time, it doesn't change any more after moving once like Main Page > Sub Page, Sub Page > Main Page.

 

Can you help me?

There are three main problems for me.

 

1. When moving a page, I would like the scrollTrigger to be reset to match the content height of the moved page. (softly)

 

2. When routing using the Link tag provided by next.js, it automatically scrolls to the top of the page. Blocking that scroll doesn't make it look smooth, and leaving it on doesn't make it look smooth. I also want this to look soft.
(Do you think page conversion can be solved through a third-party library?)

 

3. Is there a way to further optimize the overall performance of scrollTrigger in my code?

 

I wanted to provide an example with codepen, but I don't know how to show the code of the next.js project in codepen, so I attach the code.
Below is my whole code.

// react hook
import { useEffect, useRef } from "react";

// component
import Header from "@/components/Header";
import Footer from "@/components/Footer";

// style
import "@/styles/reset.css";
import "@/styles/globals.scss";

// library
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/dist/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);

// render
export default function MyApp({ Component, pageProps }) {
    const mainRef = useRef(null);
    const scrollRef = useRef(null);

    function setHeight(mainRef) {
        const target = mainRef.current;
        const headerHeight = document.getElementById("header_wrap").offsetHeight;
        target.style.marginTop = `${headerHeight}px`;
    }
  
    useEffect(() => {
        setHeight(mainRef);
        smoothScroll(scrollRef);
        window.addEventListener("resize", () => {
            setHeight(mainRef);
        });
    }, []);
  
    return (
        <div>
            <Header />
            <div className="scroll_wrap">
                <div className="scroll_container" ref={scrollRef}>
                    <main ref={mainRef}>
                        <div>
                            <Component {...pageProps} />
                        </div>
                    </main>
                    <Footer />
                </div>
            </div>
        </div>
    );
}

// smooth scroll
function smoothScroll(ref) {
    const scrollContent = ref.current;
    const scrollHeightBox = scrollContent.parentElement.parentElement;
    let height;

    // set(resize) height
    function refreshHeight() {
        const headerH = document.getElementById("header_wrap").offsetHeight;
        height = scrollContent.clientHeight + headerH;
        scrollHeightBox.style.height = height + "px";
    }
    ScrollTrigger.addEventListener("refreshInit", refreshHeight);

    gsap.to(scrollContent, {
        y: () => document.documentElement.clientHeight - height,
        ease: "none",
        onUpdate: ScrollTrigger.update,
        scrollTrigger: {
            invalidateOnRefresh: true,
            start: 0,
            end: () => height - document.documentElement.clientHeight,
            scrub: 0.6,
        },
    });

    // page route - ScrollTrigger height refresh
    const routeBtn = document.querySelectorAll(".route");
    routeBtn.forEach((elem) => {
        elem.addEventListener("click", ScrollTrigger.refresh);
    });
}

thank you.

Link to comment
Share on other sites

@OSUblake

https://codesandbox.io/s/awesome-clarke-r9m579?file=/pages/_app.js:1560-1573

Wow!! thank you. Thanks to you, I know how to create the next project example through 'codesandbox'.

 

The code in that link is not exactly the same as my project, but I have made the structure as similar as possible.

 

First of all, the first question was solved through _app.js 28~33 lines.
(It was not found in this 'codesandbox' example, but in my actual project. When you first enter the main page > go to the sub page, the height is properly reset according to the content of the sub page. And when you go back to the main page, the smoothScroll function The height set by refreshHeight is different from when you first entered. // In that state, if you press the route button to the main page once more, it works normally again. // This part is not an issue found in 'codesandbox', so my project It seems to be a problem, but if there is something to be expected, please advise.)

 

Regarding the second question, you can still see that the scrolling goes up when you move the page.
I scroll to the top, but I hope the process is not visible to the user.

 

The last question is, is there a part in my code related to scrollTrigger that I wrote wrong or should I fix it to improve performance?

 

thank you.

Link to comment
Share on other sites

1 hour ago, bs.choi said:

When you first enter the main page > go to the sub page, the height is properly reset according to the content of the sub page. And when you go back to the main page, the smoothScroll function The height set by refreshHeight is different from when you first entered.

 

I'm not sure without being to see the issue. Do you have images on any of the pages that might be affecting the layout after they load?

 

1 hour ago, bs.choi said:

Regarding the second question, you can still see that the scrolling goes up when you move the page.
I scroll to the top, but I hope the process is not visible to the user.

 

Maybe you could do a transition to make the transition less noticeable.

https://dev.to/anxiny/page-transition-effect-in-nextjs-9ch

 

1 hour ago, bs.choi said:

The last question is, is there a part in my code related to scrollTrigger that I wrote wrong or should I fix it to improve performance?

 

Looks good to me. 👍

 

Link to comment
Share on other sites

@OSUblake

7 hours ago, OSUblake said:

문제를 보지 않고는 확실하지 않습니다. 페이지가 로드된 후 레이아웃에 영향을 줄 수 있는 이미지가 있습니까?

This part found the problematic component. Let's try to solve it ourselves!

 

7 hours ago, OSUblake said:

전환을 덜 눈에 띄게 만들기 위해 전환을 수행할 수 있습니다.

https://dev.to/anxiny/page-transition-effect-in-nextjs-9ch

I've been thinking about doing this for a while, but I didn't want to use a third-party library, so I was looking for another way, thank you!

 

7 hours ago, OSUblake said:

나에게 좋아 보인다. 👍

useEffect(() => {
    ScrollTrigger.refresh();
});

Is it ok to refresh the scrollTrigger every time the page is routed in _app.js like this?
If you take a picture on the console, it does not operate repeatedly dozens of times, but only once.

 

 

And maybe this is the last question.
Thank you so much for the great answers so far.

 

There are anchors like tabs that change their associated contents when clicked.

 

Is there any performance problem with a method like below code??
When refreshed with onStart, it is sometimes not reset to the changed height, so onUpdate was used.

function anchor(e) {
    const target = e.currentTarget;
    const tabNum = target.getAttribute("data-tab");
    const tabCon = document.getElementsByClassName("tab_contents_box");
    const offsetY = document.getElementById("header_wrap").offsetHeight;

    clear(target.parentElement);
    target.parentElement.classList.add(`${styles.active}`);

    gsap.to(window, {
        duration: 1,
        scrollTo: {
            y: tabCon,
            offsetY: offsetY - 1,
        },
        ease: "power4.inOut",
        onUpdate: () => ScrollTrigger.refresh(),
    });

    setTabNum(tabNum);
}

If you look at the console, it takes about 60 console shots while changing the contents to fit each anchor, and then stops.

 

Thank you for your hard work over the past few days with my questions using the translator.

Link to comment
Share on other sites

3 hours ago, bs.choi said:

There are anchors like tabs that change their associated contents when clicked.

 

Is there any performance problem with a method like below code??
When refreshed with onStart, it is sometimes not reset to the changed height, so onUpdate was used.

 

It's not ideal, but if the layout is changing duration scroll, then that's the only option.

 

3 hours ago, bs.choi said:

I've been thinking about doing this for a while, but I didn't want to use a third-party library, so I was looking for another way, thank you!

 

It's a small library. Without it you would have to manually do all the routing without the <Link> component. You would have to fade the current view out, then push the new path, and then in the next view, animate it in. It's probably going to be a lot of work.

 

3 hours ago, bs.choi said:
useEffect(()=>{ScrollTrigger.refresh();});  
    

Is it ok to refresh the scrollTrigger every time the page is routed in _app.js like this?

 

 Well, you should probably only do it on the route changes, kind of like this.

 

import { useRouter } from "next/router";

function MyApp() {
  
  const router = useRouter();
  
  const refresh = useCallback(() => ScrollTrigger.refresh(), []);

  useEffect(() => {
    router.events.on("routeChangeComplete", refresh);
    return () => router.events.off("routeChangeComplete", refresh);
  }, [router.events, refresh]);
}

 

Link to comment
Share on other sites

@OSUblake

thank you so much.

 

I found the cause of the issue with the first question.
All images used in my project have a dynamic height (ex. width: 100%; height: auto;)

 

The cause seems to be because the image is refreshed in a state where the height of the image is not recognized before the image is loaded.
The reason is, if you do not set the height to be responsive and set it to 150px, the height will be set properly in any situation.

 

So I mean. Is there no way to refresh the image after all images are loaded while maintaining the current state (height: auto) without fixing the height of the image?

 

I made all the images into promise objects and refreshed them when they were all completed through promise.all, but this didn't work.

 

And when I used 'ZachSaucier's solution in the link above, it worked fine, but when I have a lot of images, it seems to perform poorly because of too many calls.

 

However, no matter how many images I have in my project, it won't go up to 100.
So it doesn't really matter?

 

When I applied all the work done so far and took a picture of the console, the maximum number of calls in ScrollTrigger.refresh() was about 60 times.

 

(Since ssg build is carried out through next export, it cannot be preloaded using Image api.)

Link to comment
Share on other sites

53 minutes ago, bs.choi said:

For this reason I can't use <Image />

 

Sorry, I don't understand why this wouldn't work.
https://nextjs.org/docs/api-reference/next/image#onloadingcomplete

 

I'm not familiar with next.js Image, but that's what I saw as the solution here.

https://github.com/vercel/next.js/issues/20368#issuecomment-985944527

 

1 hour ago, bs.choi said:

And when I used 'ZachSaucier's solution in the link above, it worked fine, but when I have a lot of images, it seems to perform poorly because of too many calls.

 

I wouldn't think you would need to call it if it's already complete, because that means it's height has been settled.

document.querySelectorAll("img").forEach(img => {
  if (img.complete) {
    // ScrollTrigger.refresh();
  } else {
    img.addEventListener('load', imgLoaded => ScrollTrigger.refresh());
  }
});

 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...