Search code examples
javascriptreactjsreact-router-dom

Custom Cursor not appearing in all pages in React


I am working on a portfolio website and am looking to implement a custom cursor. Currently the cursor is working on the Profile page but when I navigate to a project page, the cursor reverts to default. I have tried looking at previous posts and other articles which suggested I use the useContext hook to share the cursor but i have been unable to implement this correctly so far. Everytime i navigate to a Project page, the cursor is the default cursor (I have checked disabling it in css too). Would someone be able to help suggest a solution for this issue please? The cursor colour and size changes on any element that has the textEnter/textLeave attributes. Any help would be greatly appreciated.

My code is below:

Index.js

const router = createBrowserRouter([


 {
    path: "/",
    element: <Home />,
    errorElement: <ErrorPage />,
  },
  {
    path: "projects/:projectId",
    element: <Project />,
    loader: ({ params }) => {
      return ProjectData.find((project) => project.id == params.projectId);
    },
  },

]);
root.render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>
  </React.StrictMode>
);

App.js

function App() {


 return (
    <div>
      <CursorContextProvider>
        <ScrollToTop>
          <Cursor />
          <main className="App bg-blue-950 min-h-screen" />
        </ScrollToTop>
      </CursorContextProvider>
    </div>
  );
}

export default App;

Profile.js

const Profile = () => {
  const { scrollY } = useScroll();
  const introOpacity = useTransform(
    scrollY,
    [0, 5, 25, 50, 75, 80],
    [1, 0.8, 0.6, 0.4, 0.2, 0]
  );
  const bioOpacity = useTransform(
    scrollY,
    [90, 150, 210, 280, 320, 360],
    [0, 0.2, 0.4, 0.6, 0.8, 1]
  );

  // const cursor = useContext(CursorContext);

  // console.log(cursor);

  const location = useLocation();

  const [mousePosition, setMousePosition] = useState({
    x: 0,
    y: 0,
  });

  const [cursorVariant, setCursorVariant] = useState("default");

  const mouseMove = (e) => {
    setMousePosition({ x: e.clientX, y: e.clientY });
  };

  useEffect(() => {
    window.addEventListener("mousemove", mouseMove);
    return () => {
      window.removeEventListener("mousemove", mouseMove);
    };
  }, [location]);

  const textEnter = () => {
    setCursorVariant("text");
  };

  const textLeave = () => {
    setCursorVariant("default");
  };

  const linkEnter = () => {
    setCursorVariant("link");
  };

  const variants = {
    default: {
      x: mousePosition.x - 16,
      y: mousePosition.y - 16,
      backgroundColor: "rgb(206, 67, 159)",
    },
    text: {
      height: 120,
      width: 120,
      x: mousePosition.x - 60,
      y: mousePosition.y - 0,
      backgroundColor: "rgb(206, 67, 159)",
      mixBlendMode: "lighten",
    },
    link: {
      height: 60,
      width: 60,
      x: mousePosition.x - 30,
      y: mousePosition.y - 30,
      backgroundColor: "rgb(206, 67, 159)",
      mixBlendMode: "lighten",
    },
  };
  return (
    <main>
      <div className="grid grid-cols-4 gap-4 profile">
        <aside class="h-full">
          <div
            className="ml-12 m-5 profile-image-container bg-"
            onMouseEnter={textEnter}
            onMouseLeave={textLeave}
          >
            <img
              src={profile}
              alt="profile"
              className="rounded-full mt-4"
            ></img>
            <p className="text-left mt-8 bio-text">
              Former Product Owner who has made the switch to Full Stack
              development
              <img src={technologist} className="emoji"></img>
            </p>
            <div className="flex flex-col details-container mt-1">
              <h4 className="underline">My details:</h4>
              <div className="mt-4 flex flex-row justify-start items-center">
                <img src={linkedin} className="language"></img>
                <p
                  className="grow text-start ms-8"
                  onMouseEnter={linkEnter}
                  onMouseLeave={textLeave}
                >
                    See my Linkedin here
                  </a>
                </p>
              </div>
              <div className="mt-4 flex flex-row justify-start items-center">
                <img src={cv} className="language"></img>
                <p
                  className="grow text-start ms-8"
                  onMouseEnter={linkEnter}
                  onMouseLeave={textLeave}
                >
                  <a href="#">See my CV here </a>
                </p>
              </div>
              <div className="mt-4 flex flex-row justify-start items-center">
                <img src={github} className="language"></img>
                <p
                  className="grow text-start ms-8"
                  onMouseEnter={linkEnter}
                  onMouseLeave={textLeave}
                >
                </p>
              </div>
              <div className="mt-4 flex flex-row justify-start items-center">
                <img src={gmail} className="language"></img>
                <p
                  className="grow text-start ms-8"
                  onMouseEnter={linkEnter}
                  onMouseLeave={textLeave}
                >
                </p>
              </div>
            </div>
            <div className="mt-5 languages-container">
              <h4 className="underline">Languages:</h4>
              <div className="flex flex-col">
            </div>
          </div>
        </aside>
        <section class="col-start-2 col-end-10">
          <motion.div
            className="cursor"
            variants={variants}
            animate={cursorVariant}
          ></motion.div>
          <div
            className="col-span-3 ms-20"
            onMouseEnter={textEnter}
            onMouseLeave={textLeave}
          >
            <motion.div
              style={{ opacity: introOpacity }}
              className="w-full h-96"
            >
              <h1 className="text-left mt-24 w-full">
               Nice to meet you!
              </h1>
              <p className="text-left mt-8">
                Scroll to learn a little more....
              </p>
            </motion.div>
            <div className="scroll-container">
              <div className="icon-scroll"></div>
            </div>
            <section>
              <motion.div style={{ opacity: bioOpacity }} className="w-8/12">
                <p className="text-left">
                  <h3
                    className="underline"
                    onMouseEnter={textEnter}
                    onMouseLeave={textLeave}
                  >
                    About me
                  </h3>
                    <LanguagesList />
              </motion.div>
              <motion.div>
                <p className="text-left">
                  <h3 className="underline">Projects</h3>
                  <Projects onMouseEnter={linkEnter} onMouseLeave={textLeave} />
                </p>
              </motion.div>
            </section>
          </div>
        </section>
      </div>
    </main>
  );
};

export default Profile;

Project.js

const Project = (props) => {
// const [setCursorVariant] = useContext(CursorContext)
const project = useLoaderData();

return (
    <div className="bg-blue-950 min-h-full m-2 projectPage">
        <Header/>
        <ScrollToTop/>
            <div className="projectInfoCard">
                <Link to={"/"}>Go back to home</Link>
                <Link to={"/"}>See source code</Link>
                <div class="flex flex-col justify-center">
                    <h1>{`${project.Name}`}</h1>
                    <CarouselContainer project={project}/>
                    <p>{`${project.Description}`}</p>
                    <TechnologyList languages={project.TechStack}/>
                </div>
            </div>
            <Footer/>
    </div>
)

}

export default Project;

Cursor.js

export const CursorContext = createContext();

const Cursor = () => {
        const [mousePosition, setMousePosition] = useState({
          x: 0,
          y: 0,
        });

        // const location = useLocation();
      
        const [cursorVariant, setCursorVariant] = useState("default");
      
        const mouseMove = (e) => {
          setMousePosition({ x: e.clientX, y: e.clientY });
        };
      
        useEffect(() => {
          window.addEventListener("mousemove", mouseMove);
          return () => {
            window.removeEventListener("mousemove", mouseMove);
          };
        }, []);
      
       const textEnter = () => {
          setCursorVariant("text");
        };
      
        const textLeave = () => {
          setCursorVariant("default");
        };
      
        const linkEnter = () => {
          setCursorVariant("link");
        };

        
        const variants = {
            default: {
            x: mousePosition.x - 16,
            y: mousePosition.y - 16,
            backgroundColor: "rgb(206, 67, 159)",
            },
            text: {
            height: 120,
            width: 120,
            x: mousePosition.x - 60,
            y: mousePosition.y - 0,
            backgroundColor: "rgb(206, 67, 159)",
            mixBlendMode: "lighten",
            },
            link: {
            height: 60,
            width: 60,
            x: mousePosition.x - 30,
            y: mousePosition.y - 30,
            backgroundColor: "rgb(206, 67, 159)",
            mixBlendMode: "lighten",
            },
        };

}

export default Cursor;

Home.js

  const Home = (props) => {
  const [textEnter, textLeave, linkEnter, cursorVariant, variants] =
    useContext(CursorContext);
  return (
    <Profile
      textEnter={textEnter}
      textLeave={textLeave}
      linkEnter={linkEnter}
      cursorVariant={cursorVariant}
      variants={variants}
    />
  );
};

Solution

  • From what I can see of your code you are not rendering the App component in the ReactTree with the routes and routed content. If you then try to render App on only one of the routes, then navigating away from that route will also remove the cursor context from the ReactTree.

    App should be rendered in the ReactTree such that it is always available to the components below it, e.g. the router, routes, and routed content.

    Rename App to something more informative, like AppLayout, and have it consume and render a children prop. I suspect you want the routes/content to be rendered into the main element, but children just needs to be rendered somewhere in AppLayout.

    AppLayout

    function AppLayout({ children }) {
      return (
        <div>
          <CursorContextProvider>
            <ScrollToTop />
            <Cursor />
            <main className="App bg-blue-950 min-h-screen">
              {children}
            </main>
          </CursorContextProvider>
        </div>
      );
    }
    
    export default App;
    

    index.html

    Wrap the RouterProvider in the AppLayout so the cursor context is available to all routes, e.g. the entire application.

    const router = createBrowserRouter([
     {
        path: "/",
        element: <Home />,
        errorElement: <ErrorPage />,
      },
      {
        path: "projects/:projectId",
        element: <Project />,
        loader: ({ params }) => {
          return ProjectData.find((project) => project.id == params.projectId);
        },
      },
    ]);
    
    root.render(
      <React.StrictMode>
        <AppLayout>
          <RouterProvider router={router} />
        </AppLayout>
      </React.StrictMode>
    );