javascriptreactjsreact-grid-layoutwaypoint

Determine which section is currently dispayed with react and Waypoint


I have quite simple react application:

App.js:

render() {
 return (
   <BaseLayout />
 );
}

BaseLayout:

import React, { useEffect, useState } from 'react';
import Navbar from "./Navbar";
import Section1 from "./section1/Section1";
import Section2 from "./section2/Section2";
import Section3 from "./section3/Section3";
import { Box, Grid } from "@mui/material";
import Fade from "react-awesome-reveal";
import { Waypoint } from 'react-waypoint';

where I simply display all sections at once and then show them with Fade animation. All is good. But I also want to highlight the current displayed section on the Navbar and change it accordingly during scrolling (currenlty highlighting happens only during clicking)

  return (
    <Box className={darkMode ? Style.dark : Style.light}>
      <Grid
        container
        display={"flex"}
        flexDirection={"column"}
        minHeight={"100vh"}
        justifyContent={"space-between"}
      >
        <Grid item>
          <Navbar darkMode={darkMode} handleClick={handleToggleDarkMode} />
        </Grid>
        <Grid item flexGrow={1} className={Style.firstContentRow}>
          <Section1 />
        </Grid>
        <Grid item flexGrow={1}>
          <Fade bottom triggerOnce={true} className={Style.secondContentRow}>
            <Section2 />
          </Fade>
        </Grid>
        <Grid item flexGrow={1} className={Style.firstContentRow}>
          <Fade bottom triggerOnce={true}>
            <Section3 />
          </Fade>
        </Grid>
        <Waypoint
          onEnter={({ previousPosition, currentPosition, event }) => {
            // PreviousPosition:below
            // CurrentPosition:inside
            // The above fields just return either `below` or `inside` and *NOT* for each section.
            console.log("PreviousPosition:" + previousPosition);
            console.log("CurrentPosition:" + currentPosition);

            // I see no usefull data here as well, did I miss something?
            console.log("Event:" + event);
          }}
        />
      </Grid>
    </Box>
  );

for this I think I need to use Waypoint and some his event which should allow me to know which particular section is displayed now and then change a class name on a particular NavBar button accordingly. However, the information provided by waypoint event doesn't look enough. I can't find how can I understand which particular section is displayed at this point. Did I miss something? Also, the event is triggered not for each section, as I can see and only for the last one


Solution

  • To answer the question:

    <Waypoint/> won't help to achieve what you want, it is not used for that, it is in fact a component that you can put inside your jsx.

    Works in all containers that can scroll, including the window.

    when you scroll, you can think of it as a point that once reached, left, or changes position, the corresponding event is triggered.
    the previous position when you enter this point is always "below" here, because you have put <Waypoint/> at the end of the grid.
    the event object is not useful as well for what you want, if you check event.doccument you will find the whole scrollable element and not the last displayed element on the screen.

    event - the native scroll event that triggered the callback. May be missing if the callback wasn't triggered as the result of a scroll.

    so it is usually used

    to build features like lazy loading content, infinite scroll, scrollspies, or docking elements to the viewport on scroll.

    Solution:

    you can use three <Waypoint/> each one before each section and a state to indicate which is the last waypoint entred (the order of the section displayed):

    const [currentSection, setCurrentSection] = useState(1);
    

    then create a useEffect hook to run each time currentSection is updated:

    useEffect(() => { 
        console.log("current section is updated and this is its order ", currentSection);
        // now you have the number indicates of the current section you can manage how to make your nav bar react to that  
    }, [currentSection]);
    

    JSX:

    <Box>
      <Waypoint
        onEnter={() => {
          setCurrentSection(1);
        }}
      />
      <Grid
        container
        display={"flex"}
        flexDirection={"column"}
        minHeight={"100vh"}
        justifyContent={"space-between"}
      >
        <Grid
          item
          flexGrow={1}
          style={{ height: "800px", background: "red" }}
        >
          <div>section 1</div>
        </Grid>
        <Waypoint
          onEnter={() => {
            setCurrentSection(2);
          }}
        />
        <Grid
          item
          flexGrow={1}
          style={{ height: "800px", background: "white" }}
        >
          <div>section 2</div>
        </Grid>
        <Waypoint
          onEnter={() => {
            setCurrentSection(3);
          }}
        />
        <Grid
          item
          flexGrow={1}
          style={{ height: "800px", background: "green" }}
        >
          <div>section 3</div>
        </Grid>
      </Grid>
    </Box>
    

    Alternative (without waypoint):

    I used to manage this with an event listener on scrolling and refs.

    you want to give a ref and an id to each grid section container, also a state to indicate which section is currently on the screen:

    const firstRef = useRef();
    const secondRef = useRef();
    const ThirdRef = useRef();
    const [currentSection, setCurrentSection] = useState();
    

    when the component mounts we setCurrentSection to the first section and set our event listener:

    useEffect(() => {
      setCurrentSection(firstRef.current);
    
      window.addEventListener("scroll", handleScroll);
      return () => {
        window.removeEventListener("scroll", handleScroll);
      };
    }, []);
    

    the handleScroll function will run each time you scroll and update the currentSection state, but likely this will rerender the component only when currentSection gets a new value :

    const handleScroll = () => {
      const scrollPosition = window.scrollY || document.documentElement.scrollTop;
      const windowHeight = window.innerHeight;
    
      const sectionPositions = [
        firstRef.current,
        secondRef.current,
        ThirdRef.current
      ];
    
      const currentSection = sectionPositions.find(
        (section) =>
          scrollPosition >= section.offsetTop &&
          scrollPosition < section.offsetTop + windowHeight
      );
    
      if (currentSection) {
        setCurrentSection(currentSection);
      }
    };
    

    finally, we create a useEffect hook that will be triggered each time the value of currentSection is updated:

    useEffect(() => {
      if (currentSection) {
        console.log("current section is updated and this is the id of the current one: ", currentSection.id);
        // now you have the id of the current section you can manage how to make your nav bar react to that
      }
    }, [currentSection]);
    

    Full example:

    const Test = () => {
      const firstRef = useRef();
      const secondRef = useRef();
      const ThirdRef = useRef();
    
      const [currentSection, setCurrentSection] = useState();
    
      const handleScroll = () => {
        const scrollPosition = window.scrollY || document.documentElement.scrollTop;
        const windowHeight = window.innerHeight;
        const sectionPositions = [ firstRef.current, secondRef.current, ThirdRef.current ];
        const currentSection = sectionPositions.find(
          (section) =>
            scrollPosition >= section.offsetTop && scrollPosition < section.offsetTop + windowHeight
        );
    
        if (currentSection) {
          setCurrentSection(currentSection);
        }
      };
    
      useEffect(() => {
        setCurrentSection(firstRef.current);
        window.addEventListener("scroll", handleScroll);
        return () => {
          window.removeEventListener("scroll", handleScroll);
        };
      }, []);
    
      useEffect(() => {
        if (currentSection) {
          console.log("current section is updated and this is the id of the current one: ", currentSection.id );
          // now you have the id of the current section you can manage how to make your nav bar react to that
        }
      }, [currentSection]);
    
      return (
        <div className="App">
          <Box>
            <Grid
              container
              display={"flex"}
              flexDirection={"column"}
              minHeight={"100vh"}
              justifyContent={"space-between"}
            >
              <Grid
                id={1}
                item
                flexGrow={1}
                style={{ height: "800px", background: "red" }}
                ref={firstRef}
              >
                <div>section 1</div>
              </Grid>
              <Grid
                id={2}
                item
                flexGrow={1}
                style={{ height: "800px", background: "white" }}
                ref={secondRef}
              >
                <div>section 2</div>
              </Grid>
              <Grid
                id={3}
                item
                flexGrow={1}
                style={{ height: "800px", background: "green" }}
                ref={ThirdRef}
              >
                <div>section 3</div>
              </Grid>
            </Grid>
          </Box>
        </div>
      );
    };