不要轻易在 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 值重复变化的问题,问题依然存在!

总结

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

相关推荐
Tiffany_Ho几秒前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
太阳花ˉ1 小时前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy2 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd2 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo3 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式