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} /> ); };
问题分析
- 上下文未全局生效:
App.js未在路由中被使用,CursorContextProvider没有包裹整个应用路由,导致上下文无法在所有页面共享;同时Cursor组件没有正确实现上下文Provider,也没有返回光标DOM元素。 - 光标状态分散:
Profile组件自己维护了一套光标状态,没有复用Cursor组件的上下文状态,页面切换后状态直接丢失。 - 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 = () => {
相关产品推荐
相关产品推荐

