注意!使用props给子组件传参需要多想一步

前言

大家好,我是寻找光的sxy,又是充实的一天开发日啊!!!!在本周的开发中,我遇到了一个关于子组件传参的小坑......

现象

先看一段核心代码:父组件 Demo2 引用了子组件 Com1,但没有给 Com1 传递 obj 参数;子组件内部用解构赋值给 obj 设了默认值 {},并通过 useEffect 监听 obj 的变化、打印日志:

js 复制代码
import { useEffect, useState } from "react"; 
/** 父组件 */ 
const Demo2 = () => { 
    return ( <div> 
        <h1>Demo2</h1> 
        {/* 父组件未给子组件传 obj 参数 */} 
        <Com1 /> </div> 
     ); 
}; 
export default Demo2; 

/** 子组件 */ 
const Com1 = (props: any) => { 
    // 解构赋值,给 obj 设默认值 {} 
    const { obj = {} } = props; 
    const [nn, setNn] = useState(3); 
    // 监听 obj 变化,打印日志 
    useEffect(() => { 
        console.log(`useEffect触发,obj是: ${JSON.stringify(obj)}`); 
    }, [obj]); 
    // 点击按钮修改 nn 的值 
    const handleClick = () => { setNn(Math.random()); }; 
    return ( 
        <div> 
            <button onClick={handleClick}>Com1, nn is {nn}</button> 
        </div> 
    ); 
};

此时大家猜猜,当点击按钮时,控制台会不会执行console打印?

我的预期: useEffect 只监听 obj,而 obj 既没从父组件接收参数,也没在子组件内部修改,理应只在初始化时打印一次日志;点击按钮修改 nn,和 obj 无关,不该触发打印。

实际情况: 初始化打印一次后,每次点击按钮都会触发 useEffect 打印

这就埋下了隐患:如果后续在这个 useEffect 里加业务逻辑(比如接口请求、DOM 操作),会导致这些逻辑被频繁执行,不仅浪费性能,还可能引发意外 Bug。

解决

发现了以上的问题,我先尝试了两种简单直接的方案,用于快速"止血":

1、给子组件显式传默认值

给子组件显式传一个空对象

js 复制代码
<Com1 /> // 改动前 
<Com1 obj={{}}/> // 改动后

2、去掉子组件传参的初始值

直接去掉子组件解构的默认值

js 复制代码
const { obj = {} } = props; // 改动前 
const { obj } = props; // 改动后

以两种方法都能解决且实现预期效果;

原因

那么为什么会多次打印呢?

相信聪明的你一下子就能关注到问题的关键点:useEffect

我们在useEffect中添加了依赖项,只有依赖项发生变化的时候,才会执行打印逻辑;

回顾一下 useEffect 监听依赖项的逻辑:

  • 依赖项为基本类型时,值发生变化,useEffect就会监听到,且触发;
  • 当依赖项为引用类型时,引用地址发生变化,useEffect也会监听到,且触发;

这里的监听项是一个引用类型,那基本就可以确定了,应该是每次点击引用地址都发生了变化了。

那么问题来了,引用地址是在哪一步发生了变化?

相信聪明的你很快就能注意到以下这段代码:

js 复制代码
const { obj = {} } = props;

当点击按钮执行 setNn(Math.random()) 时,会触发子组件 Com1 的 重新渲染------ 此时子组件内部的代码会重新执行一遍,包括这句解构赋值。

  • 因为父组件没传 obj,所以每次重新渲染时,都会执行 obj = {},创建一个 新的空对象
  • 新对象的引用地址和上一次渲染的 obj 地址不同;
  • useEffect 检测到依赖 obj 的地址变了,就会触发日志打印。

再看解决方案的本质

两种方案之所以有效,核心都是 阻止 "重新渲染时创建新 obj"

  • 方案 1:父组件传递 obj={{}} 后,子组件解构时会直接用父组件传的对象(地址固定),不再创建新对象;
  • 方案 2:移除默认值后,obj 为 undefined(基本类型,值固定),地址自然不会变。

延伸思考

上面的小问题已经完美解决了,再解决后,我又想到了一种别的常用的场景,代码如下:

js 复制代码
const Com1 = () => {

  const [nn2, setNn2] = useState({ n: 1 });
  const handleClick2 = () => {
    setNn2({ n: 1 });
  };
  useEffect(() => {
    console.log(`useEffect触发,nn2是: ${nn2}`);
  }, [nn2]);

  return (
    <div>
      <button onClick={handleClick2}>改引用类型 {nn2.n}</button>
    </div>
  );
};

现象:点击按钮, useEffect 每次都触发

虽然 setNn2 的值都是 {n:1},但 每次创建的都是新对象,引用地址不同------useEffect 检测到地址变化,就会触发打印。

这和之前的子组件传参问题本质一致:都是 "引用类型的地址被意外修改" 导致的 useEffect 误触发。

总结

使用useEffect监听引用类型需谨慎:

    • 警惕 "隐式创建新引用": 比如解构赋值设默认值(const { obj = {} } = props)、函数内定义对象(setNn2({ n: 1 })),这些操作在重新渲染时会生成新地址;
    • 优先 "固定引用地址": 如果引用类型的值不变,尽量让它的地址固定(比如父组件传默认值、用 useMemo 缓存对象);
    • 复杂场景用 useMemo/useCallback: 如果需要监听的引用类型是计算得出的,用 useMemo 缓存(对象 / 数组);如果是函数,用 useCallback 缓存,避免地址频繁变化。
相关推荐
我是天龙_绍2 小时前
什么时候用ref,什么时候用reactive?
前端
古夕2 小时前
微前端跨应用中通用前端业务模块的实现
前端·javascript·vue.js
AndyLaw2 小时前
<a>标签下载文件 download 属性无效?原来问题出在这里
前端·javascript
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 19 - Reactive:reactive 的基础实现
前端·vue.js
TZOF2 小时前
TypeScript的新类型(二):unknown
前端·后端·typescript
caicai_lf_niuniu2 小时前
VUE3+element plus 实现表格行合并
前端
李宏伟~2 小时前
uniapp生成二维码组件全能组件复制即用
前端·uni-app
TZOF2 小时前
TypeScript的新类型(三):never
前端·后端·typescript
余防2 小时前
文件上传漏洞(二)iis6.0 CGI漏洞
前端·安全·web安全·网络安全