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

总结

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

相关推荐
想吃火锅10057 小时前
【leetcode】405.数字转换为十六进制数js
开发语言·javascript·ecmascript
原则猫9 小时前
HOOKS 背后机制
前端
码语智行9 小时前
首页导航跳转功能深度解析-系统内和系统外
前端
阿猫的故乡10 小时前
Vue过渡动画从入门到装X:淡入淡出、滑动、列表动画、第三方库全搞定
前端·javascript·vue.js
IManiy10 小时前
总结之Vibe Coding前端骨架
前端
小和尚敲木头10 小时前
vue3 vite动态拼接图片路径
javascript
JS菌10 小时前
AI Agent 沙箱双层防护体系:从权限过滤到内核隔离的完整实现
前端·人工智能·后端
Aphasia31110 小时前
从输入URL到页面展示全流程
前端·面试
我叫黑大帅11 小时前
前端如何竖屏固定视口背景
前端·javascript·面试
abcy07121311 小时前
python pandas csv异步后台清洗前端优先返回成功信息
前端·python·pandas