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

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无法捕捉到两次状态变化,从而动画起始位置错误。

具体修改步骤

  1. 修复鼠标移动事件的状态更新:原代码中setBox会丢失active和color状态,改为函数式更新保留原有状态。
  2. 拆分状态更新流程:点击新按钮时,先设置新坐标、颜色并关闭动画(active: false),等待DOM更新后再重新开启动画(active: true)。
  3. 优化点击事件逻辑:区分点击同一按钮(切换动画状态)和不同按钮(重置并触发新动画)。

修改后的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 10:55:13