前段时间跟几个做 React 的前端朋友谈及 React Hooks 中的心智负担问题,得到的答案让我很是惊讶,因为他们说没有感受到 React Hooks 带来的心智负担。
我突然就有点自闭了?难道是我自己的问题?
不得不说,React Hooks 的出现的确给 React 开发者带来了很多方便。但是在实际的使用过程中,我也的确发现它在带来一系列方便的同时,也带来了很多令我困扰以及不爽的地方。
这种感觉,在使用 React 其他相关的轮子(Redux、Mobox、Dva等等)时也经常遇到,那就是要么是隔靴搔痒只解决问题的一部分,要么是在解决一个问题的同时,又会引入新的问题,总之就是不让你爽的干净利落。
下面我就介绍一下我在实际项目中使用 React Hooks 遇到的那些心智负担,供大家参考指正。
陷阱一:引用旧的变量
先来看一个简单的 Hooks 使用例子。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
实现的功能呢很简单,组件安装的时候注册一个定时器,每秒加1,然后组件卸载的时候清除定时器。
乍一看没问题,但是实际上呢,页面显示的始终是1。这是因为组件每次渲染时,都会创建一个新的变量 count,但是 useEffect 里面的函数是一个闭包,里面引用的 count 变量始终都是第一次渲染时的变量(值始终为1)。
如果只是解决这个问题,倒是很简单,使用状态改变的函数式调用Api即可:把setCount(count + 1)
改成setCount(count => count + 1)
。
但是如果 useEffect 里面的函数稍微复杂一点,引用的是多个状态互相依赖,就没法使用这种方式了:
function Counter() {
const [count, setCount] = useState(0);
const [varA, setVarA] = useState('');
useEffect(() => {
const id = setInterval(() => {
if (varA === 'xxx') {
setCount(count + 1);
} else {
setCount(count + 2);
}
}, 1000);
return () => clearInterval(id);
}, []);
//...
}
那该怎么办呢?
将 useState 换成 useRef?倒是能解决变量引用的问题,但是 useRef 有个问题,就是改变 ref 的值组件并不会重新渲染,界面也不会更新,pass!
当然还是有解决办法的。
第一种,使用 useReducer,这里不作示例了, 这里的问题倒是能解决,但是如果 dispatch 带参数的话又回到了问题本身。
第二种,使用 useEffect 的依赖数组:
function Counter() {
const [count, setCount] = useState(0);
const [varA, setVarA] = useState('');
useEffect(() => {
const id = setInterval(() => {
if (varA === 'xxx') {
setCount(count + 1);
} else {
setCount(count + 2);
}
}, 1000);
return () => clearInterval(id);
}, [count, varA]);
//...
}
但是这种方式有个新的问题,就是 count 和 varA 的每一次改变,定时函数都会重新创建一次,在这个例子里勉强能用,但是在有些场景就有问题了,后面讨论。
陷阱二:useEffect 到底加不加依赖?
import { showTip } from 'tip';
function ExampleTip() {
const [varA, setVarA] = useState('');
const [varB, setVarB] = useState('');
useEffect(() => {
showTip({ a: varA, b: varB })
}, [varA]);
}
上面这类的需求,其实在开发中很常见,那就是某一个状态(varA)改变时,我们需要做某件事(这里的例子是弹出一个tip框),但是做这件事又需要引用其他的状态(varB等)。
但是 varB 改变时,我们不应该弹出 tip 框。
这时候,varB 是不应该加入依赖数组的。
另一种情况:
import { fetch } from 'api';
function Example() {
const [varA, setVarA] = useState('');
const [varB, setVarB] = useState('');
useEffect(() => {
window.addEventListener('click', () => {
doSomeThing({ a: varA, b: varB })
});
return () => {
// remove listener
}
}, [varA,varB]);
}
在这里,useEffect 里,对于varB我们想使用的不是useEffect调用时的值,而是varB最新的值。但是,useEffect里引用的变量始终是varA改变时的值,varB改变之后 useEffect 里并不知道。
这时候,我们就需要把 varB 也加入依赖数组,varB改变时重新监听。
对于很简单的代码,我们一般能够清楚哪个该加哪个不该加。但是每次编写的时候我们都要考虑哪些该加哪些不该加,无疑加重了心智负担。
而且,如果没有工具的保证,就会很容易出错,这也是为什么我们需要 eslint、typescript 等工具的原因。
官方也意识到了这个问题,为我们提供了相应的 eslint 插件:eslint-plugin-react-hooks。
但现实问题是,对于任何一个状态变量,存在着该加
入依赖数组和不该加
入依赖数组两种情况,并且这两种情况,工具是没法从代码层面进行区分的!
怎么办呢?官方的建议是都开启exhaustive-deps
规则,也就是说只要是在 useEffect 中引用到的状态变量(除开ref),都应该加入依赖数组!
官方文档里也说,未来版本,或许会在构建时自动加入第二个参数。
这意味着,将所有 useEffect 里使用到的变量都加入依赖数组,似乎是官方推荐的、最正确的选择。而且你想要使用 eslint 相关工具的话,也要使用这种方式。
这解决了引用旧变量的问题,让我们不用去思考该不该加入依赖数组的问题.
但又引入了新的问题,比如上面showTip那个,我们不得不加入一些额外的代码:
import { showTip } from 'tip';
function ExampleTip() {
const [varA, setVarA] = useState('');
const [varB, setVarB] = useState('');
const varBRef = useRef(varB);
varBRef.current = varB;
useEffect(() => {
showTip({ a: varA, b: varBRef.current })
}, [varA]);
}
useRef 变量不必要加入依赖数组,因为引用不变。
但是如果 useEffect 里引用的变量有很多个呢?每一个都加一个ref岂不是很麻烦?
我一般使用这种方法:
function ExampleTip() {
const [varA, setVarA] = useState('');
const [varB, setVarB] = useState('');
const showTipRef = useRef(null);
showTipRef.current = () => {
showTip({ a: varA, b: varBRef.current });
}
useEffect(() => {
showTipRef.current && showTipRef.current();
}, [varA]);
}
问题是解决了,但是怎么也不爽。
陷阱三:引用到底变没变?
对于同一个组件里,我们定义的 useRef 变量可以不加入依赖数组,因为 eslint 插件能识别出这是一个不变的引用。
但是,看下面这个组件:
function Example(props) {
const { id, fetch } = props;
useEffect(() => {
fetch(id);
}, [id, fetch]);
}
我们需要在id改变的时候,执行fetch方法。但是 fetch 引用改变的时候,我们一般是不需要重新 fetch 的。
即使传入的 fetch 引用是不变的,但是 eslint 插件并不能识别出来,所以它还是要求将 fetch 加入依赖数组。
这样,乍一看这个组件,你能区分出 fetch 会不会变吗?你能保证只有在 id 改变的时候 fetch 吗?
看起来是能跑,但是怎么看怎么不放心……
陷阱四:依赖引入的死循环风险
function Child(props) {
const { onAppear, onLeave } = props;
useEffect(() => {
onAppear();
return () => {
onLeave();
}
}, [onAppear, onLeave])
}
function Parent() {
const [count, setCount] = useState(0);
const appearItem = () => {
setCount(count + 1);
}
const leaveItem = () => {
setCount(count - 1);
}
return (
<>
<Child onAppear={appearItem} onLeave={leaveItem} />
<Child onAppear={appearItem} onLeave={leaveItem} />
</>
);
}
上面这个例子,单独开每个组件似乎没有问题,但是跑起来就会死循环。因为 Child 组件里的 onAppear, onLeave 引用每次渲染都会变!
为了解决这个问题,我们不得不将 appearItem, leaveItem通过 useCallback 包裹起来:
function Child(props) {
const { onAppear, onLeave } = props;
useEffect(() => {
onAppear();
return () => {
onLeave();
}
}, [onAppear, onLeave])
}
function Parent() {
const [count, setCount] = useState(0);
const appearItem = useCallback(() => {
setCount(count + 1);
}, [count]);
const leaveItem = useCallback(() => {
setCount(count - 1);
}, [count]);
return (
<>
<Child onAppear={appearItem} onLeave={leaveItem} />
<Child onAppear={appearItem} onLeave={leaveItem} />
</>
);
}
看似解决了,但还是会死循环,因为 count 每次渲染都会变!这时候又不得不上 useRef 大法了……
这种隐形的死循环风险,其实在 hooks 代码中很常见。为了规避这些风险,我们不得不想很多。
陷阱五:误导性的参数
function Component(props) {
const varA = useRef(props.a);
const [varB] = useState(props.b);
}
上面这个简单的组件,又有什么问题呢?
乍一眼看去,varA,varB的值是根据props的值动态计算的,因为每次渲染 useRef,useState 都会调用一次,props.a 和 props.b 都会传入一次。
但是事实上,React 只会使用第一次传入的值!
这对 useRef, useState 这样经常用的 hooks 函数也许影响不大,因为你一直在注意,都形成了本能,知道它的运行方式了。
但是对于一个自定义hooks来说呢?
function Component() {
const [value, setValue] = useState('');
const test = useCustomHook(value);
}
这个vluae,到底是第一个值会被使用,还是所有的值都会被使用呢?
总结
写的好累!上面描述的这些例子,都是我在实际工作中经常会遇到的问题。除了这些,还有其他一些问题,只不过很多都不是三言两语能够描述清楚的。
这只是根据我的水平所得到的理解,其中有些也许不是特别正确,欢迎批评指正!
总之,给我个人的感觉就是,React Hooks 给我不爽的主要有以下几点:
- 违背直觉:代码实际的运行方式和你第一眼看上去的感觉差别很大。
- 需要想很多:很多时候,我们不得不去考虑一些本来不该我们考虑、而应该是框架层面解决的问题。
- 不安全感:即使你花很多心思写出来的组件,很多时候回头看写下的代码时,总是觉得哪里不对,不是很有安全感。
仔细寻思一下,其实所有问题的根源,是 React 函数时组件机制所限:每次组件渲染,组件里的所有代码都会被重新调用一次。
而 Vue 的组合式Api 和 React hooks 如此类似,但是它之所以没有这么多烦恼,主要是因为它的 setup 只会在整个组件的生命周期内执行一次。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!