2
votes

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>
  )
}
2

2 Answers

2
votes

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);
    };
  }, []);
0
votes

you can declare multiple useEffect() within a component. I think this will help you.

in your header component, leave the first useEffect() as it is. Add a second one like so:

useEffect(() => {
  ... code to update state
}, [updatedProp]);

The key thing here is the updatedProp item in the array. this tells the useEffect to trigger when the particular prop or variable is changed. Replace the updatedProp with the variables / props you'd like this useEffect to trigger.