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

React useEffect依赖项问题:exhaustive-deps警告解析与疑问

React Hooks exhaustive-deps 警告问题解析与解决方案

警告产生的原因

ESLint的exhaustive-deps规则会检查useEffect回调中用到的外部变量,要求将它们加入依赖数组,确保这些值变化时useEffect能重新执行,避免闭包过时导致的逻辑错误。

  1. 首次警告是因为useEffect的render函数中用到了radius和drawBall,但依赖数组里没加这两个值;
  2. 添加后再次警告,是因为drawBall定义在组件顶层,每次组件渲染都会重新创建这个函数,导致其引用地址变化,进而触发useEffect频繁重新执行。

解决方案

方案1:将drawBall移至useEffect内部

把drawBall放到useEffect回调里,这样它就不会成为外部依赖,同时配合useRef保存图片实例(避免每次渲染都新建Image):

import { useRef, useEffect } from 'react';

export default function Ball({ radius }: BallProps) {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    // 用useRef保存图片实例,避免每次渲染重新创建
    const ballRef = useRef<HTMLImageElement | null>(null);

    useEffect(() => {
        // 仅初始化一次图片
        if (!ballRef.current) {
            const ball = new Image();
            ball.src = "/billiard.png";
            ballRef.current = ball;
        }

        const canvas = canvasRef.current;
        if (!canvas || !ballRef.current) return;
        
        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        // 将drawBall定义在useEffect内部
        const drawBall: DrawBallProps = (ctx, radius) => {
            if (!ballRef.current) return;
            ctx.drawImage(ballRef.current, 0, 0, radius * 2, radius * 2);
            // 这里添加动画逻辑
        };
        
        const render = () => {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawBall(ctx, radius);
            requestAnimationFrame(render);
        };

        const animationId = requestAnimationFrame(render);

        // 清理函数:取消动画帧,避免内存泄漏
        return () => {
            cancelAnimationFrame(animationId);
        };
    }, [radius]); // 仅依赖radius,drawBall内部定义无外部依赖

    return (
        <canvas ref={canvasRef} width={100} height={100} style={{ border: '1px solid black' }} />
    );
}

方案2:用useCallback包裹drawBall

通过useCallback缓存函数引用,确保只有当依赖项变化时才重新创建drawBall,同时用useRef保存图片实例:

import { useRef, useEffect, useCallback } from 'react';

export default function Ball({ radius }: BallProps) {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const ballRef = useRef<HTMLImageElement | null>(null);

    // 初始化图片,仅执行一次
    useEffect(() => {
        const ball = new Image();
        ball.src = "/billiard.png";
        ballRef.current = ball;
    }, []);

    // 用useCallback缓存drawBall,依赖仅为ballRef(引用稳定)
    const drawBall = useCallback<DrawBallProps>((ctx, radius) => {
        if (!ballRef.current) return;
        ctx.drawImage(ballRef.current, 0, 0, radius * 2, radius * 2);
        // 这里添加动画逻辑
    }, [ballRef]);
    
    useEffect(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;
        
        const ctx = canvas.getContext('2d');
        if (!ctx) return;

        const render = () => {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawBall(ctx, radius);
            requestAnimationFrame(render);
        };

        const animationId = requestAnimationFrame(render);

        return () => {
            cancelAnimationFrame(animationId);
        };
    }, [drawBall, radius]); // 依赖稳定后的drawBall和radius

    return (
        <canvas ref={canvasRef} width={100} height={100} style={{ border: '1px solid black' }} />
    );
}

不添加依赖的影响

  • 遗漏radius依赖:当父组件传入的radius更新时,useEffect不会重新执行,render函数会一直使用旧的radius值,球的大小无法随数据更新,导致UI与数据不一致。
  • 遗漏drawBall依赖:由于闭包特性,effect中的render函数会捕获首次渲染时的drawBall引用。后续组件重新渲染时,即使drawBall的逻辑修改(比如调整绘制方式),或者其依赖的变量(如ball)变化,render里调用的还是旧的drawBall,导致动画逻辑不更新、绘制结果不符合预期,甚至出现难以排查的bug。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 04:17:10