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

React应用中如何在requestAnimationFrame循环内使用响应式变量?

解决React中Canvas动画无法响应Leva控件更新的问题

核心问题是闭包捕获导致的旧值引用:你的render函数是在初始的useEffect(依赖为空数组)中创建的,它捕获的是组件第一次渲染时的size值。之后Leva控件更新size时,这个旧的render函数并不会重新生成,所以动画循环里始终用的是初始的size,而非最新值。

解决方案1:用Ref同步最新size值(推荐,不中断动画)

通过useRef保存最新的size数值,让render函数能直接访问到更新后的值,同时保持动画循环不中断:

import { useRef, useEffect } from "react";
import { useControls } from 'leva';

export default function SquareCanvas() {
    const canvasRef = useRef(null);
    const { size } = useControls({ 
        size: { value: 10, min: 1, max: 50}, 
    });
    // 创建ref同步最新size
    const sizeRef = useRef(size);
    let canvas, ctx;
    // 保存动画ID用于清理
    const animationIdRef = useRef(null);

    // 监听size变化,实时更新ref的current值
    useEffect(() => {
        sizeRef.current = size;
    }, [size]);

    useEffect(() => {
        canvas = canvasRef.current;
        ctx = canvas.getContext('2d');
        
        setSize();
        const handleResize = setSize;
        window.addEventListener('resize', handleResize);

        ctx.fillStyle = 'red';
        
        function render(time) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            const x = canvas.width / 2 + Math.sin(time * .005) * 100;
            const y = canvas.height / 2;
            // 使用ref中的最新size值
            ctx.fillRect(x, y, sizeRef.current, sizeRef.current);

            animationIdRef.current = requestAnimationFrame(render);
        }

        animationIdRef.current = requestAnimationFrame(render);

        // 清理函数:移除监听并取消动画,避免内存泄漏
        return () => {
            window.removeEventListener('resize', handleResize);
            cancelAnimationFrame(animationIdRef.current);
        }
    }, []);

    function setSize() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    }

    return <div><canvas ref={canvasRef}></canvas>{ size }</div>
}

解决方案2:将size加入useEffect依赖(动画会重启)

如果可以接受动画在size变化时短暂重启,也可以把size加入useEffect的依赖数组,每次size更新时重新初始化动画循环:

import { useRef, useEffect } from "react";
import { useControls } from 'leva';

export default function SquareCanvas() {
    const canvasRef = useRef(null);
    const { size } = useControls({ 
        size: { value: 10, min: 1, max: 50}, 
    });
    let canvas, ctx;
    const animationIdRef = useRef(null);

    useEffect(() => {
        canvas = canvasRef.current;
        ctx = canvas.getContext('2d');
        
        setSize();
        const handleResize = setSize;
        window.addEventListener('resize', handleResize);

        ctx.fillStyle = 'red';
        
        function render(time) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            const x = canvas.width / 2 + Math.sin(time * .005) * 100;
            const y = canvas.height / 2;
            ctx.fillRect(x, y, size, size);

            animationIdRef.current = requestAnimationFrame(render);
        }

        animationIdRef.current = requestAnimationFrame(render);

        return () => {
            window.removeEventListener('resize', handleResize);
            cancelAnimationFrame(animationIdRef.current);
        }
    }, [size]); // 加入size作为依赖,触发重新初始化

    function setSize() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
    }

    return <div><canvas ref={canvasRef}></canvas>{ size }</div>
}

关键说明

  • 方案1通过sizeRef同步最新值,动画循环不会中断,体验更流畅,适合需要持续播放的动画场景。
  • 方案2每次size更新都会重新创建render函数并重启动画,适合对动画连续性要求不高的场景。
  • 两种方案都添加了动画和resize监听的清理逻辑,避免内存泄漏。

内容的提问来源于stack exchange,提问作者TheCat

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 06:35:05