注意!使用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 缓存,避免地址频繁变化。
相关推荐
jump6806 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信10 分钟前
我们需要了解的Web Workers
前端
brzhang15 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu34 分钟前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花36 分钟前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐1 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
BBB努力学习程序设计2 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
Zyx20072 小时前
深拷贝:JavaScript 中对象复制的终极解法
javascript