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

总结

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

相关推荐
艾小逗1 小时前
vue3中的effectScope有什么作用,如何使用?如何自动清理
前端·javascript·vue.js
小小小小宇4 小时前
手写 zustand
前端
Hamm4 小时前
用装饰器和ElementPlus,我们在NPM发布了这个好用的表格组件包
前端·vue.js·typescript
明似水5 小时前
Flutter 弹窗队列管理:支持优先级的线程安全通用弹窗队列系统
javascript·安全·flutter
小小小小宇5 小时前
前端国际化看这一篇就够了
前端
大G哥5 小时前
PHP标签+注释+html混写+变量
android·开发语言·前端·html·php
whoarethenext5 小时前
html初识
前端·html
小小小小宇6 小时前
一个功能相对完善的前端 Emoji
前端
m0_627827526 小时前
vue中 vue.config.js反向代理
前端
Java&Develop6 小时前
onloyoffice历史版本功能实现,版本恢复功能,编辑器功能实现 springboot+vue2
前端·spring boot·编辑器