不要轻易在 React 里监听引用类型默认值的解构属性,一不小心就会入坑

前言

最近组内同事在分享的时候提到了 React 解构 Props 时可能会出现一些坑,当时只是默默记在心底,但后面发现在造轮子和写通用性组件的时候又碰到了类似的情况,所以我就杂糅一下分享给大家

你可以在这里看到并运行所有的代码

入坑

解构属性默认值

先说入坑的前置条件:解构属性默认值,上代码

jsx 复制代码
const ComA = (props) => {
  const { num = 1, config } = props;
  return <>{num}</>;
};
// or
const ComA = ( { num = 1, config }) => {
  return <>{num}</>;
};
// use
const App = () => {
  return <ComA />;
}

大多数 Props 解构可能就是这样,确实很方便也很优雅,对于具有默认值的 Props 这是一种常见的做法

引用类型默认值

突然有一天你觉得 ComAconfig 可能也需要一个默认值,默认配置是一个很合理的需求,后面 ComA 被迭代成了

jsx 复制代码
const ComA = (props) => {
  const { num = 1, config = { a: 1 } } = props;
  return <>{num}</>;
};

还是很优雅 然后继续迭代,某个需求的 useEffect 突然要依赖 config.a,由于 config 只有一个属性,所以代码补全的时候直接帮你补上了 config,如下

jsx 复制代码
const ComA = (props) => {
  const { num = 1, config = { a: 1 } } = props;
  useEffect(() => {
    console.log("config.a", config.a);
  }, [config]);

  return <>{num}</>;
};

但是随后问题袭来

re-render 导致的重复渲染

num 改变的时候会触发 ComA 的重复渲染,这个时候的 config重新被赋值{ a: 1} 将会是一个全新的对象,而是说 useEffect 的函数会重新执行一次,因为依赖更新,如下

jsx 复制代码
// App 开始控制 ComA num props 的值
export default function App() {
  const [num, setNum] = useState(0);

  return (
    <div className="App">
      <button onClick={() => setNum((prev) => prev + 1)}>setNum</button>
      <br />
      <ComA num={num} />
    </div>
  );
}

类似的情况还可能出现于 config 内部属性是引用类型的情况,比如

css 复制代码
config = { type: { key: '', value: '' } }

但是究其原因都是对象重新创建的问题

解决方案

静态变量

config 的值在 re-render 被重新创建主要是因为解构属性默认值的问题,可以用一个外部的静态变量引用去避免值在解构时候重新创建,如下

jsx 复制代码
// config = { a: 1 } -> config = DEFAULT_CONFIG
const DEFAULT_CONFIG = { a: 1 };
const ComA = (props) => {
  const { num = 1, config = DEFAULT_CONFIG } = props;
  useEffect(() => {
    console.log("config.a", config.a);
  }, [ob]);

  return <>{num}</>;
};

useMemo

静态变量的方案已经很简洁了,但是如果有人在 ComA 组件内部修改了 Props,那么在 <ComA /> -> <ComA config={config} /> -> <ComA /> 的过程中就有可能出现旧状态的复用,如下

jsx 复制代码
import { useState, useEffect } from "react";

const DEFAULT_CONFIG = { a: 1 };
const ComA = (props) => {
  const { num = 1, config = DEFAULT_CONFIG } = props;
  useEffect(() => {
    console.log("config.a", config.a);
  }, [config]);

  return (
    <>
      <button
        onClick={() => {
          config.a = 0;
        }}
      >
        !!set config.a!!
      </button>
      <br />
      {num}
    </>
  );
};

export default function App() {
  const [num, setNum] = useState(0);
  const [config, setConfig] = useState();

  return (
    <div className="App">
      <button onClick={() => setNum((prev) => prev + 1)}>setNum</button>
      <button onClick={() => setConfig({ a: 2 })}>initConfig</button>
      <button onClick={() => setConfig(undefined)}>resetConfig</button>
      <br />
      <ComA num={num} config={config} />
    </div>
  );
}

在上面的代码中

  1. <ComA /> 内部去修改 config 的默认值
  2. 外部给 <ComA /> 一个 config 的初始值
  3. 重置 <ComA />config

最终结果 符合预期:<ComA /> 内部的 config 重置后回到 { a: 1 } 实际情况:<ComA /> 内部的 config 重置后保留了内部修改的值 { a: 0 }

因为 DEFAULT_CONFIG 是静态变量,也可以理解为全局变量,修改后除非刷新页面,不然值会一直保持

useMemo 可以解决这个问题,每次 props 更新的时候决定要不要给一个新的初始值,它避免

  1. re-render 时因为 props.config 没有更新,所以不影响 config
  2. props.config 置为空时,可以返回新的 config
jsx 复制代码
const DEFAULT_CONFIG = { a: 1 };
const ComA = (props) => {
  const { num = 1 } = props;

  const config = useMemo(() => {
    return props.config ? props.config : { ...DEFAULT_CONFIG };
  }, [props.config]);

  useEffect(() => {
    console.log("config.a", config.a);
  }, [config]);
  ...
}

组件内部修改 Props 的值是不被建议,不符合规范的做法,但是谁能决定写 React 的人写代码的姿势呢?

useRef

这是上面两个方案能够解决的场景的子集,它只能解决部分场景

有些时候我们可能并不是想依赖 config,可能我们只是需要读取它的值,但是因为解构而导致的 re-render 导致 useEffect 之类的 hooks 频繁被触发,这个时候可以用 useRef 去减少一个依赖,如下

jsx 复制代码
import { useState, useEffect, useRef } from "react";

// before
// useEffect(() => {
//   console.log("num", num, "config.a", config.a);
// }, [num, config]);
const ComA = (props) => {
  const { num = 1, config = { a: 1 } } = props;
  const configRef = useRef();
  configRef.current = config.a;
  useEffect(() => {
    const config = configRef.current;
    console.log("num", num, "config.a", config.a);
  }, [num]);

  return <>{num}</>;
};

export default function App() {
  const [num, setNum] = useState(0);

  return (
    <div className="App">
      <button onClick={() => setNum((prev) => prev + 1)}>setNum</button>
      <br />
      <ComA num={num} />
    </div>
  );
}

这个案例里的 useEffect 监听了 num 而不是 config,在 num 变化时再通过 useRef 拿到最新的 config,而不去关心 config 的值是不是最新的,避免 config 因为解构重复更新

这个解决方案没有解决 config 值重复变化的问题,问题依然存在!

总结

优雅的解构可能会带来意外的坑

相关推荐
原则猫18 分钟前
前端基础大厦
前端
陈随易1 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·后端·程序员
SoaringHeart2 小时前
Flutter进阶:基于 EasyRefresh 的下拉刷新封装 n_easy_refresh_mixin.dart
前端·flutter
IT_陈寒4 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰5 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
山河木马5 小时前
渲染管线-计算得到gl_Position(顶点着色器)之后续GPU流程
javascript·webgl·图形学
竹林8185 小时前
用 The Graph 查询链上数据实战:从手搓 RPC 到 Subgraph,我的 NFT 项目数据加载快了 10 倍
前端·javascript
妙码生花6 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
Awu12276 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
咪库咪库咪7 小时前
Vue3-生命周期
前端