React渲染前设置状态问题:背景动画坐标异常修复咨询
React中clip-path圆形动画起始位置错误的修复方案
问题描述
我正在开发一个简单的React应用,包含3个不同颜色的按钮:点击按钮时,会触发从按钮自身位置发起的clip-path圆形CSS动画,改变父div的背景;再次点击则恢复原背景。
问题在于,每次点击新按钮时,状态未能在渲染前更新,导致新背景动画沿用旧按钮的坐标,出现动画起始位置错误的情况。我了解到React没有渲染前的状态设置时机,且不建议在componentWillMount中设置状态,因此想咨询是否有可行的修复方案?还是React无法实现这类需求?
原代码
App.js
import './App.css'; import {useEffect, useState} from 'react'; function App() { const [box, setBox] = useState({active: false, color: '', left: '', top: ''}); // const [globalCoords, setGlobalCoords] = useState({x: 0, y: 0}) useEffect(() => { const handleWindowMouseMove = event => { if ((event.x < 350 && event.x > 250) && (event.y > 250 && event.y < 350)) { setBox({left: '300px', top: '300px'}) } else if ((event.x < 1600 && event.x > 1500) && (event.y > 50 && event.y < 150)) { setBox({left: '1550px', top: '100px'}) } else if ((event.x < 1100 && event.x > 1000) && (event.y > 500 && event.y < 600)) { setBox({left: '1050px', top: '550px'}) } }; document.addEventListener('mousemove', handleWindowMouseMove); return () => { document.removeEventListener('mousemove', handleWindowMouseMove); }; }, []); // console.log(globalCoords); return ( <section> <div style={{'--color': box.color, '--left': box.left, '--top': box.top}} className={box.active ? 'box active' : 'box'}></div> <div style={{backgroundColor: box.color === 'orange' ? 'black' : ''}} onClick={() => setBox({active: !box.active, color: 'orange', left: '300px', top: '300px'})} className={box.active ? (box.color === 'orange' ? 'pulse1' : 'pulse1 disable') : 'pulse1'}> <span style={{'--i': 0}}></span> <span style={{'--i': 1}}></span> <span style={{'--i': 2}}></span> <span style={{'--i': 3}}></span> </div> <div style={{backgroundColor: box.color === 'turquoise' ? 'black' : ''}} onClick={() => setBox({active: !box.active, color: 'turquoise', left: '1550px', top: '100px'})} className={box.active ? (box.color === 'turquoise' ? 'pulse2' : 'pulse2 disable') : 'pulse2'}> <span style={{'--i': 0}}></span> <span style={{'--i': 1}}></span> <span style={{'--i': 2}}></span> <span style={{'--i': 3}}></span> </div> <div style={{backgroundColor: box.color === 'red' ? 'black' : ''}} onClick={() => setBox({active: !box.active, color: 'red', left: '1050px', top: '550px'})} className={box.active ? (box.color === 'red' ? 'pulse3' : 'pulse3 disable') : 'pulse3'}> <span style={{'--i': 0}}></span> <span style={{'--i': 1}}></span> <span style={{'--i': 2}}></span> <span style={{'--i': 3}}></span> </div> </section> ); } export default App;
App.css
:root { --i: 0; --top: 0; --left: 0; --color: transparent; } *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } section { min-height: 100vh; width: 100vw; background-color: #102e37; position: absolute; top: 0; left: 0; } .box { position: absolute; height: 100%; width: 100%; background-color: var(--color); transition: clip-path 1s linear; clip-path: circle(0% at var(--left) var(--top)); } .box.active { clip-path: circle(150% at var(--left) var(--top)); } .pulse1 { position: absolute; width: 100px; height: 100px; background-color: orange; border-radius: 50%; top: 250px; left: 250px; animation: intro 5s; animation-delay: 0s; } .pulse1 span { position: absolute; height: 100%; width: 100%; border-radius: inherit; background-color: inherit; opacity: .8; animation: pulseAnimate 4s ease-out infinite; animation-delay: calc(1s * var(--i)); } @keyframes pulseAnimate { 100% { opacity: 0; transform: scale(2); } } .pulse2 { position: absolute; width: 100px; height: 100px; background-color: turquoise; border-radius: 50%; top: 50px; left: 1500px; animation: intro 5s; animation-delay: 0s; } .pulse2 span { position: absolute; height: 100%; width: 100%; border-radius: inherit; background-color: inherit; opacity: .8; animation: pulseAnimate 4s ease-out infinite; animation-delay: calc(1s * var(--i)); } .pulse3 { position: absolute; width: 100px; height: 100px; background-color: red; border-radius: 50%; top: 500px; left: 1000px; animation: intro 5s; animation-delay: 0s; } .pulse3 span { position: absolute; height: 100%; width: 100%; border-radius: inherit; background-color: inherit; opacity: .8; animation: pulseAnimate 4s ease-out infinite; animation-delay: calc(1s * var(--i)); } .pulse1.disable { animation: seconds .8s both; animation-delay: 0s; } .pulse2.disable { animation: seconds .8s both; animation-delay: 0s; } .pulse3.disable { animation: seconds .8s both; animation-delay: 0s; } @keyframes seconds { 0% { opacity: 1; } 100% { opacity: 0; } } @keyframes intro { 0% { opacity: 0; } 100% { opacity: 1; } }
修复方案
问题核心是:切换按钮时,需要先将动画重置到新按钮的坐标(circle(0% at 新坐标)),再触发展开动画(circle(150% at 新坐标))。但原代码中状态更新是批量的,导致CSS无法捕捉到两次状态变化,从而动画起始位置错误。
具体修改步骤
- 修复鼠标移动事件的状态更新:原代码中
setBox会丢失active和color状态,改为函数式更新保留原有状态。 - 拆分状态更新流程:点击新按钮时,先设置新坐标、颜色并关闭动画(
active: false),等待DOM更新后再重新开启动画(active: true)。 - 优化点击事件逻辑:区分点击同一按钮(切换动画状态)和不同按钮(重置并触发新动画)。
修改后的App.js代码
import './App.css'; import { useEffect, useState } from 'react'; function App() { const [box, setBox] = useState({ active: false, color: '', left: '', top: '' }); useEffect(() => { const handleWindowMouseMove = event => { if (event.x < 350 && event.x > 250 && event.y > 250 && event.y < 350) { // 函数式更新,保留原有状态 setBox(prev => ({ ...prev, left: '300px', top: '300px' })); } else if (event.x < 1600 && event.x > 1500 && event.y > 50 && event.y < 150) { setBox(prev => ({ ...prev, left: '1550px', top: '100px' })); } else if (event.x < 1100 && event.x > 1000 && event.y > 500 && event.y < 600) { setBox(prev => ({ ...prev, left: '1050px', top: '550px' })); } }; document.addEventListener('mousemove', handleWindowMouseMove); return () => { document.removeEventListener('mousemove', handleWindowMouseMove); }; }, []); // 监听坐标/颜色变化,重置后触发新动画 useEffect(() => { if (!box.active && box.color) { const timer = setTimeout(() => { setBox(prev => ({ ...prev, active: true })); }, 0); return () => clearTimeout(timer); } }, [box.color, box.left, box.top, box.active]); const handleButtonClick = (color, left, top) => { setBox(prev => { // 点击同一按钮,切换动画状态 if (prev.active && prev.color === color) { return { ...prev, active: false }; } // 点击不同按钮,先重置坐标颜色并关闭动画 return { active: false, color, left, top }; }); }; return ( <section> <div style={{ '--color': box.color, '--left': box.left, '--top': box.top }} className={box.active ? 'box active' : 'box'} ></div> <div style={{ backgroundColor: box.color === 'orange' ? 'black' : '' }} onClick={() => handleButtonClick('orange', '300px', '300px')} className={box.active ? (box.color === 'orange' ? 'pulse1' : 'pulse1 disable') : 'pulse1'} > <span style={{ '--i': 0 }}></span> <span style={{ '--i': 1 }}></span> <span style={{ '--i': 2 }}></span> <span style={{ '--i': 3 }}></span> </div> <div style={{ backgroundColor: box.color === 'turquoise' ? 'black' : '' }} onClick={() => handleButtonClick('turquoise', '1550px', '100px')} className={box.active ? (box.color === 'turquoise' ? 'pulse2' : 'pulse2 disable') : 'pulse2'} > <span style={{ '--i': 0 }}></span> <span style={{ '--i': 1 }}></span> <span style={{ '--i': 2 }}></span> <span style={{ '--i': 3 }}></span> </div> <div style={{ backgroundColor: box.color === 'red' ? 'black' : '' }} onClick={() => handleButtonClick('red', '1050px', '550px')} className={box.active ? (box.color === 'red' ? 'pulse3' : 'pulse3 disable') : 'pulse3'} > <span style={{ '--i': 0 }}></span> <span style={{ '--i': 1 }}></span> <span style={{ '--i': 2 }}></span> <span style={{ '--i': 3 }}></span> </div> </section> ); } export default App;
修复原理
- 点击不同按钮时,先将
active设为false,同时更新坐标和颜色,此时.box的clip-path变为circle(0% at 新坐标)。 - 通过
useEffect监听状态变化,在坐标和颜色更新完成后,延迟0毫秒(等待DOM渲染完成)将active设为true,此时clip-path从circle(0% at 新坐标)过渡到circle(150% at 新坐标),动画从新按钮位置正确发起。 - 函数式更新确保鼠标移动时不会丢失
active和color状态,避免意外的动画中断。
内容的提问来源于stack exchange,提问作者user19308961
相关产品推荐
相关产品推荐

