前言
最近组内同事在分享的时候提到了 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
这是一种常见的做法
引用类型默认值
突然有一天你觉得 ComA
的 config
可能也需要一个默认值,默认配置是一个很合理的需求,后面 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>
);
}
在上面的代码中
<ComA />
内部去修改config
的默认值- 外部给
<ComA />
一个config
的初始值 - 重置
<ComA />
的config
最终结果 符合预期:<ComA />
内部的 config
重置后回到 { a: 1 }
实际情况:<ComA />
内部的 config
重置后保留了内部修改的值 { a: 0 }
因为 DEFAULT_CONFIG
是静态变量,也可以理解为全局变量,修改后除非刷新页面,不然值会一直保持
用 useMemo
可以解决这个问题,每次 props
更新的时候决定要不要给一个新的初始值,它避免
re-render
时因为props.config
没有更新,所以不影响config
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 值重复变化的问题,问题依然存在!
总结
优雅的解构可能会带来意外的坑