1

I am creating a navigation bar with React that visually highlights the active component when it is scrolled into view by the user. So, they scroll to the 'About' component and the user icon would highlight and the home icon would return to normal. It looks like this:

enter image description here

I am trying to do this in 2 steps: (1) using the useInView hook in my App.jsx component to set the 'activeElement' with a useState hook when the user scrolls to the useInView {ref}, and (2) passing the 'activeElement' as a useState variable to the child Nav component via props and useEffect to update activeNav in the Nav component when the user is scrolling.

Here is my code for the App component, which I have been testing within the paragraph tags. Currently, activeElement is not being affected by scrolling.

const App = () => {
  const { ref, inView, entry } = useInView({
    /* Optional options */
    threshold: 0,
  });

  const [activeElement, setActiveElement] = useState('#')
  
  return (
    <>
      <Header ref={ref} setActiveElement={inView ? '#home' : '' }/>
      <Nav activeElement={activeElement}/>
      <About ref={ref} setActiveElement={inView ? '#about' : '' } />
      <p>{activeElement}</p>
      <Services ref={ref} setActiveElement={inView ? '#services' : '' } />
      <Contact ref={ref} setActiveElement={inView ? '#contact' : '' }/>
      <Footer />
      <p>{activeElement}</p>
    </>
  )
}

export default App

And here is the code for my Nav component:

const Nav = ({activeElement}) => {
  const [activeNav, setActiveNav] = useState('#');
  
  useEffect(() => {
    setActiveNav(activeElement);
  })

  return (
    <nav>
      <a href="#" onClick={() => setActiveNav('#')} className={activeNav === '#' ? 'active' : ''}><AiOutlineHome /></a>
      <a href="#about" onClick={() => setActiveNav('#about')} className={activeNav === '#about' ? 'active' : ''}><AiOutlineUser /></a>
      <a href="#experience" onClick={() => setActiveNav('#experience')} className={activeNav === '#experience' ? 'active' : ''}><HiOutlineBookOpen /></a>
      <a href="#services" onClick={() => setActiveNav('#services')} className={activeNav === '#services' ? 'active' : ''}><FaUncharted /></a>
      <a href="#contact" onClick={() => setActiveNav('#contact')} className={activeNav === '#contact' ? 'active' : ''}><RiMessage2Line /></a>
    </nav>
  )
}

export default Nav

What is wrong with my useInView execution? And am I passing the activeElement variable to the Nav component correctly?

Thanks for taking the time to read this through.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
BettyWhite
  • 119
  • 12

3 Answers3

2

** SOLUTION: **

After some digging, and with thanks to the commenter, Ali Mirzaei, for helping to identify where the issue was occuring, we found 2 problems:

  1. I needed a separate useInView hook for each element being observed.
  2. Using 'ref' on a component call was creating an error: "Warning: Function components cannot be given refs. Attempts to access this ref will fail." So, I used the answer from https://stackoverflow.com/a/65756885/13471663 to pass the ref as a prop named innerRef

Working code is as follows:

const App = () => {
  const { ref, inView } = useInView();

  const { ref: ref1, inView: inView1 } = useInView();

  const { ref: ref2, inView: inView2 } = useInView();

  const { ref: ref3, inView: inView3 } = useInView();

  const { ref: ref4, inView: inView4 } = useInView();

  const [activeElement, setActiveElement] = useState('#')

  useEffect(() => {
    if (inView) {
      setActiveElement('#home');
      console.log('home');
    };
    if (inView1) {
      setActiveElement('#about')
      console.log('about');
    };
    if (inView2) {
      setActiveElement('#experience')
      console.log('experience');
    };
    if (inView3) {
      setActiveElement('#services')
      console.log('services');
    };
    if (inView4) {
      setActiveElement('#contact')
      console.log('contact');
    };
  }, [inView, inView1, inView2, inView3, inView4])
  
  return (
    <>
      <Header innerRef={ref} />
      <Nav activeElement={activeElement}/>
      <About innerRef={ref1} />
      <p>{activeElement} {inView.toString()}</p>
      <Experience innerRef={ref2} />
      <Services innerRef={ref3} />
      <Contact innerRef={ref4} />
      <Footer />
      <p>{activeElement}</p>
    </>
  )
}

export default App

And for the Nav component:

const Nav = ({activeElement}) => {
  const [activeNav, setActiveNav] = useState('#home');
  
  useEffect(() => {
    setActiveNav(activeElement);
  })

  return (
    <nav>
      <a href="#" onClick={() => setActiveNav('#home')} className={activeNav === '#home' ? 'active' : ''}><AiOutlineHome /></a>
      <a href="#about" onClick={() => setActiveNav('#about')} className={activeNav === '#about' ? 'active' : ''}><AiOutlineUser /></a>
      <a href="#experience" onClick={() => setActiveNav('#experience')} className={activeNav === '#experience' ? 'active' : ''}><HiOutlineBookOpen /></a>
      <a href="#services" onClick={() => setActiveNav('#services')} className={activeNav === '#services' ? 'active' : ''}><FaUncharted /></a>
      <a href="#contact" onClick={() => setActiveNav('#contact')} className={activeNav === '#contact' ? 'active' : ''}><RiMessage2Line /></a>
    </nav>
  )
}

export default Nav

And here is an example of the innerRef use from a component:

const About = ({ innerRef }) => {
  return (
    <section id='about'>
      <div ref={innerRef}>
        About
      </div>
    </section>
  )
}

Hope that helps anyone out there experiencing the same issue!

BettyWhite
  • 119
  • 12
1

fisrt of all what is setActiveElement={inView ? '#home' : '' } in your components?

you have to pass different refs for each component you want to track if it's in the viewport so form react-intersection-observer documents:

import * as React from "react";
// @ts-ignore Wrong type
import { createRoot } from "react-dom/client";
import { useInView } from "react-intersection-observer";
import ScrollWrapper from "./elements/ScrollWrapper";
import "./styles.css";

function App() {

  const { ref, inView } = useInView({
    threshold: 0
  });

  const { ref: ref2, inView: inView2 } = useInView({
    threshold: 0
  });

  return (
    <ScrollWrapper inView={inView}>
      <div ref={ref} className="inview-block">
        <h2>
          Element is inside the viewport: <strong>{inView.toString()}</strong>
        </h2>
      </div>
      <div ref={ref2} className="inview-block">
        <h2>
          Element is inside the viewport: <strong>{inView2.toString()}</strong>
        </h2>
      </div>
    </ScrollWrapper>
  );
}

const root = createRoot(document.getElementById("root"));
root.render(<App />);


ali mirzaei
  • 309
  • 1
  • 3
  • 12
1

SOLUTION + Animated Scrolling React + Typescript:

I've done the same as @BettyWhite, but also I add an animation to each component when the intersection observer is active. So when the user is scrolling, the component detected, appear with a custom animation.

App.tsx:

const Default: React.FC<Props> = () => {
const { theme } = useTheme();
const [activeElement, setActiveElement] = useState("#");

const options = {
  rootMargin: "-200px",
  // triggerOnce: true,
};
const { ref: refHome, inView: inViewHome } = useInView(options);
const { ref: refAbout, inView: inViewAbout } = useInView(options);
const { ref: refSkills, inView: inViewSkills } = useInView(options);
const { ref: refProjects, inView: inViewProjects } = useInView(options);
const { ref: refServices, inView: inViewServices } = useInView(options);
const { ref: refTravel, inView: inViewTravel } = useInView(options);
const { ref: refContact, inView: inViewContact } = useInView(options);

useEffect(() => {
  if (inViewHome) setActiveElement("#home");
  if (inViewAbout) setActiveElement("#about");
  if (inViewSkills) setActiveElement("#skills");
  if (inViewProjects) setActiveElement("#projects");
  if (inViewServices) setActiveElement("#services");
  if (inViewTravel) setActiveElement("#travel");
  if (inViewContact) setActiveElement("#contact");
}, [
inViewAbout,
inViewContact,
inViewHome,
inViewProjects,
inViewTravel,
inViewServices,
inViewSkills,
]);

return (
  <div id={theme}>
    <NavBar activeElement={activeElement} />
    <main>
      <div className="container">
        <div ref={refHome} className={inViewHome ? "animate" : "opacity-0"}>
        <Home />
      </div>
      <div ref={refAbout} className={inViewAbout ? "animate" : "opacity-0"}>
        <About />
      </div>
      <div
        ref={refSkills}
        className={inViewSkills ? "animate" : "opacity-0"}
      >
        <Skills />
      </div>
      <div
        ref={refProjects}
        className={inViewProjects ? "animate" : "opacity-0"}
      >
        <Projects />
      </div>
      <div
        ref={refServices}
        className={inViewServices ? "animate" : "opacity-0"}
      >
        <Services />
      </div>
      <div
        ref={refTravel}
        className={inViewTravel ? "animate" : "opacity-0"}
      >
        <Travel />
      </div>
      <div
        ref={refContact}
        className={inViewContact ? "animate" : "opacity-0"}
      >
        <Contact />
      </div>
      <Footer />
    </div>
  </main>
</div>
);
};

App.ts (styles):

.animate {
  opacity: 0;
  animation: appear 1s ease forwards;
  animation-delay: 0.1s;
}

@keyframes appear {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

Then each of the Components inside have an id reference, for example, :

const Home: React.FC = () => {
  return (
    <Section className="home section" id="home">
      <div className="home__container grid">
    <div className="home__content grid">
      <Social />
      <div className="home__img"></div>
      <HomeData />
    </div>
    <ScrollDown />
  </div>
</Section>
);
};

And the same in the others components, so when the intersection is active, the component appear with the custom animation. And for the navBar I use a similar code as @BettyWhite.