Home > Software design >  How to have the useEffect and useLocation hooks sync up correctly in React using React Router?
How to have the useEffect and useLocation hooks sync up correctly in React using React Router?

Time:11-13

I am making an app where mousewheel up and down are used to navigate to and from different pre set routes.

currently the routes look like this:

main -> skills -> aboutme -> work

mousewheel-down cycles from left to right in the order above, and mousewheel-up does the same but in reverse.

this is the logic:

import React, { useState, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";

const Context = React.createContext();

function ContextProvider({ children }) {
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    window.addEventListener("wheel", (e) => handleNavigation(e));

    return () => {
      window.removeEventListener("wheel", (e) => handleNavigation(e));
    };
  }, [location]);

  function handleNavigation(e) {
    if (location.pathname.includes("/work/")) {
      return;
    } else {
      if (e.deltaY > 1 && location.pathname === "/") {
        navigate("/skills");
      } else if (e.deltaY < 1 && location.pathname === "/skills") {
        navigate("");
      } else if (e.deltaY > 1 && location.pathname === "/skills") {
        navigate("/aboutme");
      } else if (e.deltaY < 1 && location.pathname === "/aboutme") {
        navigate("/skills");
      } else if (e.deltaY > 1 && location.pathname === "/aboutme") {
        navigate("/work");
      } else if (e.deltaY < 1 && location.pathname === "/work") {
        navigate("/aboutme");
      }
    }
  }

  return <Context.Provider value={{}}>{children}</Context.Provider>;
}

export { ContextProvider, Context };

now this is working perfectly fine however in the work component I have nested routes (e.g. /work/blahblahblah) where I don't want the mouse wheel to scroll to different routes, so you can see on the first line of the handleNavigation() function I added a simple conditional where if the location.pathname contains /work/ to return out of the function and not run the logic below.

However I have found that this is not working, when I scroll to the /work route and then click to go into the nested route scrolling still runs the logic below the if statement taking me to routes which I did not intend for, if I for example manually in the address bar go into the nested route such as /work/foo and then try scrolling my if conditional works correctly and scrolling on that page does not navigate me to different routes.

for a replicable example please look here:

Edit fast-sky-kbm006

scroll with the mouse wheel down to the /work route and then click the link to take you to the /work/click-here link.

I have tried playing around with the useEffect dependency array but nothing seems to be working.

CodePudding user response:

By moving the function handleNavigation inside the useEffect and pass the named function to addEventListener and removeEventListener, it fix the issue for me.

This is because useEffect need a reference so it can clean up the previous listener when dependencies change, and start a new one.

Also, there is no need to manually pass event to a listener since it is automatically given the event object when triggered.

Lastly I took out pathname for use in useEffect so it has a lighter dependency.

Example:

const { pathname } = location;

  useEffect(() => {
    function handleNavigation(e) {
      if (pathname.includes("/work/")) {
        return;
      } else {
        if (e.deltaY > 1 && pathname === "/") {
          navigate("/skills");
        } else if (e.deltaY < 1 && pathname === "/skills") {
          navigate("/");
        } else if (e.deltaY > 1 && pathname === "/skills") {
          navigate("/aboutme");
        } else if (e.deltaY < 1 && pathname === "/aboutme") {
          navigate("/skills");
        } else if (e.deltaY > 1 && pathname === "/aboutme") {
          navigate("/work");
        } else if (e.deltaY < 1 && pathname === "/work") {
          navigate("/aboutme");
        }
      }
    }
    window.addEventListener("wheel", handleNavigation);

    return () => window.removeEventListener("wheel", handleNavigation);
  }, [pathname]);

Hopefully the above code would fix this issue for now.

However, you might also want to consider moving this event handling to a regular wrapper component, instead of in useContext.

This is because useContext is intended for sharing data between components, yet these events clearly are for handling views and does not really use it's feature.

There are many approaches, but here is a quick example:

import React, { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";
const Layout = ({ children }) => {
  const navigate = useNavigate();
  const location = useLocation();

  const { pathname } = location;

  useEffect(() => {
    function handleNavigation(e) {
      if (pathname.includes("/work/")) {
        return;
      } else {
        if (e.deltaY > 1 && pathname === "/") {
          navigate("/skills");
        } else if (e.deltaY < 1 && pathname === "/skills") {
          navigate("/");
        } else if (e.deltaY > 1 && pathname === "/skills") {
          navigate("/aboutme");
        } else if (e.deltaY < 1 && pathname === "/aboutme") {
          navigate("/skills");
        } else if (e.deltaY > 1 && pathname === "/aboutme") {
          navigate("/work");
        } else if (e.deltaY < 1 && pathname === "/work") {
          navigate("/aboutme");
        }
      }
    }
    window.addEventListener("wheel", handleNavigation);

    return () => window.removeEventListener("wheel", handleNavigation);
  }, [pathname]);

  return <>{children}</>;
};

export default Layout;

The above component can be used to wrap around your Routes and should provide same result (make sure to remove ContextProvider if testing).

CodePudding user response:

Well, first - you should not be doing this in a context provider. Here you can learn about context in react

Your bug appeared because you pass different links to handleNavigation in add and remove event listener functions. The link changes between renders, because you create new handleNavigation function each time and the variable holds a link to new place in memory. The listener doesn't get removed, old location value remains in the closure, so the bug happens

Regarding your question - the best way to do this is to make custom hook useScrollNavigation that uses useLocation, adds event listener on mount and removes on unmount. I would use this hook in a layout component that wraps all the routes that need this behavior, so it just unmounts when user navigates to different route. You also could pass an array of routes where you need this behavior, like that you won't need a layout route.

  • Related