Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to useEffect inside header component (ReactJS/Next.js)

I have a header.js component, which is present on each page in my next.js/react project.

Inside I've created a typewriter effect with its rotation, so to fire it up I've put the code inside useEffect as:

export default function Header () {

  useEffect(() => {

    let delay = 2200;
    let input = document.getElementsByClassName('myButton')[0],
        index = 0;
    const cycleText = () =>{
      let randomItem = items[Math.floor(Math.random() * items.length)];

      button.setAttribute('value', "Click " + randomItem);
      index++;
      (index === items.length) ? index = 0 : "";
      setTimeout(cycleText, delay);
    }

    cycleText();
  }, []);

  return ( ...

To recreate sort of document.ready as you'd do with jQuery, altough I'm encountering two issues doing so with useEffect, one is that if I change page, the header remains the same but the effect code looks dead (no text is shown anymore), two, if I go back to the homepage, where the code was working ok on first load, now it shows two instances of the rotation in quick success (as if there are two useEffect going at the same time one above the other, or something similar).

I'm getting closer to understand how useEffect works, I know the [] makes it run once upon page load, but I thought that was all I needed for my case, and looks like I'm wrong. Should I pass something inside [] which can fix both the issues explained above? I've tried with next.js router, passing router.pathname, to let it run again every time there's a page change, but doesn't work.

UPDATE: I've noticed the two strange behaviour described above only happen when going from the homepage to a pre-generated page (via staticPaths and staticProps) and back. If I go from the homepage to the user dashboard (serverside rendered) and back it doesn't mess up with the useEffect in the header

UPDATE 2: I found out it's the retain scroll position code I have inside _app.js which indeed targets that pre-generated page, as well as the homepage, but NOT the user dashboard page in fact. Now I have to figure out how I can retain the scroll position by also letting components to re-render, cause I've seen due to the same issue also another piece of code to make the sidebar sticky doesn't fire up

this is my _app.js code:

import React, { useRef, useEffect, memo } from 'react'
import { Provider as NextAuthProvider } from 'next-auth/client'
import { SWRConfig } from 'swr'
import Router, { useRouter } from 'next/router'
import '../public/style.scss'
import { AnimatePresence } from "framer-motion"

export default function MyApp ({ Component, pageProps }) {

  const router = useRouter()
  const retainedComponents = useRef({})

  var isRetainableRoute = false

  if (router.pathname == '/' || router.asPath.includes('/author/') || router.asPath.includes('/video/')){
    isRetainableRoute = true
  }

  // Add Component to retainedComponents if we haven't got it already
  if (isRetainableRoute && !retainedComponents.current[router.asPath]) {
    const MemoComponent = memo(Component)
    retainedComponents.current[router.asPath] = {
      component: <MemoComponent {...pageProps} />,
      scrollPos: 0
    }
  }

  // Save the scroll position of current page before leaving
  const handleRouteChangeStart = url => {
    if (isRetainableRoute) {
      retainedComponents.current[router.asPath].scrollPos = window.scrollY
    }
  }

  // Save scroll position - requires an up-to-date router.asPath
  useEffect(() => {
    router.events.on('routeChangeStart', handleRouteChangeStart)
    return () => {
      router.events.off('routeChangeStart', handleRouteChangeStart)
    }
  }, [router.asPath])

  // Scroll to the saved position when we load a retained component
  useEffect(() => {
    if (isRetainableRoute) {
      window.scrollTo(0, retainedComponents.current[router.asPath].scrollPos)
    }
  }, [Component, pageProps])

  return (
    <NextAuthProvider
      options={{
        clientMaxAge: 60,
        keepAlive: 61
      }}
      session={pageProps.session}
      >
      <SWRConfig 
        value={{
          revalidateOnFocus: false
        }}
      >
        <AnimatePresence exitBeforeEnter>
          <div>
            <div style={{ display: isRetainableRoute ? 'block' : 'none' }}>
              {Object.entries(retainedComponents.current).map(([path, c]) => (
                <div
                  key={path}
                  style={{ display: router.asPath === path ? 'block' : 'none' }}
                >
                  {c.component}
                </div>
              ))}
            </div>
            {!isRetainableRoute && <Component {...pageProps} />}
          </div>
        </AnimatePresence>
      </SWRConfig>
    </NextAuthProvider>
  )
}
like image 731
m4tt Avatar asked Sep 05 '26 05:09

m4tt


1 Answers

By calling recursively cycleText in setTimeout you create a function that never ends, even when the Header component gets unmounted (which happens when you change page). When move to a page that has again the Header component, that Header component is created and the useEffect is executed again once when it is mounted, so another occurence of the endless cycleText function is started, etc.

You can put a console.log in your cycleText function to see that it will log more and more quickly when you move to another page and come back to the one that has the Header.

I think it would be better to avoid a recursive endless function to avoid using your stack memory, and use setInterval instead. And more important: don't forget to stop your function when the component gets unmounted. For useEffect it is done when it returns, you can call clearInterval there.

You can do something like this:

  useEffect(() => {
    let delay = 2200;
    let input = document.getElementsByClassName('myButton')[0],
    index = 0;

    const interval = setInterval(() => {
      let randomItem = items[Math.floor(Math.random() * items.length)];
      button.setAttribute('value', "Click " + randomItem);
      index++;
      if(index >= items.length){
        index = 0
      }
    }, delay);

    return () => {
      clearInterval(interval);
    };
  }, []);
like image 80
Roman Mkrtchian Avatar answered Sep 07 '26 18:09

Roman Mkrtchian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!