You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React自定义光标仅在Profile页面生效,项目页面失效求助

自定义光标跨页面失效问题解决方案

问题描述

我正在开发个人作品集网站,想要实现自定义光标功能。目前光标在Profile页面可以正常工作,但导航到项目页面时,光标会恢复为默认样式。我查阅过相关帖子和文章,尝试使用useContext钩子共享光标状态,但至今未能正确实现。每次导航到Project页面,光标都会变成默认样式(我已排查过CSS禁用光标的情况)。光标会在带有textEnter/textLeave属性的元素上改变颜色和尺寸,恳请各位提供解决方案,非常感谢!

现有代码

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 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}
    />
  );
};

问题分析

  1. 上下文未全局生效:App.js未在路由中被使用,CursorContextProvider没有包裹整个应用路由,导致上下文无法在所有页面共享;同时Cursor组件没有正确实现上下文Provider,也没有返回光标DOM元素。
  2. 光标状态分散:Profile组件自己维护了一套光标状态,没有复用Cursor组件的上下文状态,页面切换后状态直接丢失。
  3. Project页面未集成光标:Project页面既没有渲染自定义光标元素,也没有绑定光标切换事件。

修复方案

1. 重构CursorContextProvider,实现全局上下文

修改Cursor.js,创建独立的Provider组件统一管理光标状态:

import { createContext, useState, useEffect } from 'react';
import { motion } from 'framer-motion';

export const CursorContext = createContext();

export const CursorContextProvider = ({ children }) => {
  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);
  }, []);

  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 (
    <CursorContext.Provider value={{ textEnter, textLeave, linkEnter }}>
      {children}
      {/* 全局渲染光标元素 */}
      <motion.div
        className="cursor fixed pointer-events-none z-50"
        variants={variants}
        animate={cursorVariant}
      />
    </CursorContext.Provider>
  );
};

2. 修改Index.js,让上下文包裹整个路由

确保所有页面都能访问上下文:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { CursorContextProvider } from './Cursor';
import Home from './Home';
import Project from './Project';
import ErrorPage from './ErrorPage';

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>
    <CursorContextProvider>
      <RouterProvider router={router} />
    </CursorContextProvider>
  </React.StrictMode>
);

3. 简化Profile组件,复用上下文状态

删除组件内部的光标状态,直接从上下文获取方法:

import { useContext } from 'react';
import { CursorContext } from './Cursor';
import { useScroll, useTransform, motion } from 'framer-motion';

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 { textEnter, textLeave, linkEnter } = useContext(CursorContext);

  return (
    <main>
      {/* 原有页面结构不变,仅保留事件绑定 */}
      <div className="grid grid-cols-4 gap-4 profile">
        <aside className="h-full">
          <div
            className="ml-12 m-5 profile-image-container"
            onMouseEnter={textEnter}
            onMouseLeave={textLeave}
          >
            {/* 内部元素结构不变 */}
          </div>
        </aside>
        <section className="col-start-2 col-end-10">
          {/* 移除组件内部的光标元素,改用全局渲染的光标 */}
          <div
            className="col-span-3 ms-20"
            onMouseEnter={textEnter}
            onMouseLeave={textLeave}
          >
            {/* 内部元素结构不变 */}
          </div>
        </section>
      </div>
    </main>
  );
};

export default Profile;

4. 改造Project页面,集成光标事件

在Project页面中绑定光标切换方法:

import { useContext } from 'react';
import { Link, useLoaderData } from 'react-router-dom';
import { CursorContext } from './Cursor';

const Project = () => {
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 23:51:51