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

总结

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

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
光头程序员7 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架