这是在项目开发过程中使用react
的function component
和hooks
遇到的问题, 平时在写代码的过程中, 整个页面级别的文件我会选择使用class
的形式书写, 而在功能或者说逻辑相对简单, 比如纯展示型的组件, 我就会选择使用function component
同时辅以hooks
Rendered more hooks than during the previous render
在描述项目的实际情况之前我们一起来看一个接地府地气的demo
, 最近全国各地都在展开疫苗的接种, 我也在清明假期期间接种了第一剂疫苗, 接种之前需要填表, 里面有一项是是否处于妊娠期
, 男女都要填, 但如果以一个电子表单的形式来处理, 那么可以在性别为女的时候才显示, 性别为男的时候则不显示, 代码如下:
import React, { Fragment, useState } from 'react';
const Vero = () => {
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
if(gender === 'male') {
return (
<Fragment>
<div>
<div>18岁以下:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
</div>
<div>
<div>60岁以上:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
</div>
<div>
<div>性别:</div>
<div>
男:
<input
type="radio"
value={'male'}
checked={gender === 'male'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
<div>
女:
<input
type="radio"
value={'female'}
checked={gender === 'female'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
</div>
</Fragment>
);
}
const [ gestation, setGestation ] = useState(1);
return (
<Fragment>
<div>
<div>18岁以下:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
</div>
<div>
<div>60岁以下:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
</div>
<div>
<div>性别:</div>
<div>
男:
<input
type="radio"
value={'male'}
checked={gender === 'male'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
<div>
女:
<input
type="radio"
value={'female'}
checked={gender === 'female'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
</div>
<div>
<div>妊娠期:</div>
<div>
是:
<input
type="radio"
value={1}
checked={gestation}
onChange={
e => {
const { target } = e;
const { value } = target;
setGestation(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!gestation}
onChange={
e => {
const { target } = e;
const { value } = target;
setGestation(+value);
}
}
/>
</div>
</div>
</Fragment>
);
};
export default Vero;
当然了, 我只是举个例子, 毕竟我们用react
进行开发, 这么样的一个需求, 无论如何也不会这么写, 毕竟任何技能你熟练到一定程度, 想犯错反而是个比较困难的事, 这不是凡尔赛哈, 我想写这个报错的例子也还是想了一下才写出来的...
咳...咳...言归正传, 此时我们修改gender
, 那么页面就会报错了:
react
检测到了Vero
调用hook
的顺序发生了改变, 如果不修复将会导致一些问题和错误, 详情请查阅hooks的规则
比上一次多渲染了些hooks
那么我来看一看, 到底怎么就比上一次多渲染了, 首先, 程序渲染的是3
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
此时由于if
判断, 最终我们的函数式组件
返回的jsx
渲染结果为:
18岁以下
60岁以上
性别
然后我们修改了性别state
: gender
, 将它的值从初始值male
修改为了female
, 那么此时Vero
函数重新执行, 最终渲染结果为:
4
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
const [ gestation, setGestation ] = useState(1);
if
条件不再满足, 不执行, 渲染结果为:
18岁以下
60岁以上
性别
妊娠期
但程序报错, 最终的渲染结果没能正常显示出来, 此时我们发现:
上一次渲染了3
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
接下来渲染了4
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
const [ gestation, setGestation ] = useState(1);
确实比上一次多, 多1
个
const [ gestation, setGestation ] = useState(1);
这是怎么回事呢?让我们来一起看看报错信息里提到的文档:
只在顶部调用hooks
不要在循环, 条件或者嵌套函数中调用hooks
. 相反, 总是在你react
函数的顶部, 在过早的return
语句之前使用hooks
. 通过遵守这个规则, 你就能确定每次渲染的时候hooks
都能按照相同的顺序被调用, 这就是为何react
能在多个useState
和useEffect
的调用中正确保持hooks
的状态的原因(如果你好奇, 我们在后面的文章中会有一个解释)
查阅这个解释之后发现答案是:
react
依赖hooks
的调用顺序
我们的例子能够正常运行是因为每次渲染hook
的渲染顺序都是一致的
回到我们自己的例子中, 之所以报错是因为顺序被打乱了, 一开始调用的hooks
的渲染我们记作Previous render
, 此时渲染的hooks
如下:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
然后我们修改了了性别
这个state
之后出发了重新渲染, 这次渲染我们记作Next render
, 此时if
判断不成立, 代码运行if
之后的语句, 于是渲染的hooks
如下:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
const [ gestation, setGestation ] = useState(1);
Next render
比Previous render
多渲染了1
个hook
:
const [ gestation, setGestation ] = useState(1);
在报错的结果中其实也有显示这两次的渲染差异:
我们的代码导致了前后hooks
渲染的不一致, 此时react
就没法依赖hooks
的调用顺序了, 因为两次的不一致了, 因此就报错了
Rendered fewer hooks than expected. This may be caused by an accidental early return statement
有Rendered more hooks
的错自然也就有Rendered fewer hooks
的错, 我们把一开始的代码做一个修改, 只改一个地方, 其他保持不变:
if(gender === 'male')
改为:
if(gender === 'female')
同样, 修改我们的性别
这个state
, 此时react
会给我们报如下的错误:
Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
为何这回是fewer
而不是more
了呢?我们根据上面的知识来看看, 首先Previous render
, 因为if
的条件修改了之后if
里面的语句无法被执行, 直接渲染出了带妊娠期
的jsx
, 也就是此次渲染渲染了4
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
const [ gestation, setGestation ] = useState(1);
而当我们修改了性别
这个state
触发重绘的时候, 此时if
条件被满足了, 执行了if
里面的语句, 后面的语句不再执行, 此时Next render
就只执行到if
语句里面的return
的位置, 因此这回只渲染了3
个hooks
, 也就是顶部的那3
个hooks
:
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
我们可以自己写一下Previous render
和Next render
, 这样会有一个更加直观的比较:
Previous render | Next render | gender | gender | isUnder18 | isUnder18 | isOver60 | isOver60 | gestation | undefined |
---|
这样就能直观的看到前后两次渲染中hooks
被渲染的情况了
当然了, 这个需求如果真的实现起来, 正确的写法是这样的:
import React, { Fragment, useState } from 'react';
const Vero = () => {
const [ gender, setGender ] = useState('male');
const [ isUnder18, setIsUnder18 ] = useState(0);
const [ isOver60, setIsOver60 ] = useState(0);
const [ gestation, setGestation ] = useState(1);
return (
<Fragment>
<div>
<div>18岁以下:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isUnder18}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsUnder18(+value);
}
}
/>
</div>
</div>
<div>
<div>60岁以下:</div>
<div>
是:
<input
type="radio"
value={1}
checked={isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!isOver60}
onChange={
e => {
const { target } = e;
const { value } = target;
setIsOver60(+value);
}
}
/>
</div>
</div>
<div>
<div>性别:</div>
<div>
男:
<input
type="radio"
value={'male'}
checked={gender === 'male'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
<div>
女:
<input
type="radio"
value={'female'}
checked={gender === 'female'}
onChange={
e => {
const { target } = e;
const { value } = target;
setGender(value);
}
}
/>
</div>
</div>
{
gender === 'female'
? (
<div>
<div>妊娠期:</div>
<div>
是:
<input
type="radio"
value={1}
checked={gestation}
onChange={
e => {
const { target } = e;
const { value } = target;
setGestation(+value);
}
}
/>
</div>
<div>
否:
<input
type="radio"
value={0}
checked={!gestation}
onChange={
e => {
const { target } = e;
const { value } = target;
setGestation(+value);
}
}
/>
</div>
</div>
)
: null
}
</Fragment>
);
};
export default Vero;
性别
state
为male
(也就是初次渲染)时:
当我们修改了性别
state
为female
时:
然后就可以愉快地去打疫苗了
在项目中的场景
在自己负责的一个项目中, 网络请求使用了graphql
(apollo-client), 同时是ssr
(nextjs)的项目, 里面还需要获取用户的地理位置, 这里使用了百度地图JavaScript API v3.0类参考_Geolocation, 这里顺便吐槽一下百度地图是真难用, 之前用过百度云存储, 文档不全, 连蒙带猜的使用, 功能提供不全, 还要去改源码, 醉了, 现在写文章的时候发现之前的定位demo
找不到了, 就附上Geolocation
这个类的文档
获取用户位置的操作是异步的, 以及我们需要在程序渲染到了客户端也就是浏览器的时候根据浏览器自带的定位接口定位, 失败了再走IP
定位, 这个逻辑百度定位API
帮我们封装好了, 我们直接使用即可, 但因为是异步的, 我们需要这么做, 流程如下:
- 先呈递一版页面给用户
- 获取用户坐标
- 在拿到定位之后请求后端接口重新获取数据
- 页面
loading
- 请求结束取消
loading
- 用新的数据重新渲染页面
页面使用了function component
, 同时使用了hooks
, 项目一直都很ok, 直到接入登录注册流程, 登录注册是另一个小伙伴写的, 他封装成了一个js
文件, 也就是说这个js
给window
上加了一个object
, 使用传统的方式, 就是传递登录注册框div
的id
的方式来使用, 我需要在程序渲染到浏览器的时候去访问window
来拿到他封装的这个object
从而初始化登录注册功能
查看上面的流程, 我需要在第6
步之后做这个动作, 因为如果在获取坐标之前做这个操作, 如果用户同意获取坐标, 此时拿到了坐标重新渲染, 那么初始化之后的登录注册框将消失, 于是只能在第二次渲染页面也就是第6
步之后来做:
if(loading) {
return <LoadingSpin />;
}
useEffect(
() => {
//初始化登录注册框
},
[]
)
这个时候就犯了上面的demo
中的错了, 我在条件判断return
之后使用了hook
, 此时调用顺序会发生变化, 最终导致react
报错, 但我如果将useEffect
则会在获取到用户坐标的时候页面重新渲染而导致登录注册初始化失效, 此时我想到了一个方法: 既然获取到坐标再去请求数据的时候要渲染loading
组件, 那么我在loading
的componentWillUnmount
生命周期中做初始化操作不就行了
说干就干:
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Spin } from 'antd';
const LoadingSpin = ({ containerStyle = {}, size, willUnmountCb }) => {
useEffect(
() => {
return () => {
if(willUnmountCb) {
willUnmountCb();
}
};
},
[]
);
return (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
...containerStyle
}}
>
<Spin size={size || 'default'}/>
</div>
);
};
LoadingSpin.propTypes = {
containerStyle: PropTypes.object,
size: PropTypes.string,
willUnmountCb: PropTypes.func
};
export default LoadingSpin;
这里的LoadingSpin
就是非常典型的function component
组件的一个例子, 纯展示型组件, 用fc
再合适不过, 哪怕会加入一些state
, 使用useState
这些hooks
也最合适不过了, 这里的useEffect
:
useEffect(
() => {
return () => {
if(willUnmountCb) {
willUnmountCb();
}
};
},
[]
);
第一个参数是个函数, 该函数如果return
了一个函数, 那么return
的这个函数将在组件卸载之前执行, 也就是我们class
组件的componentWillUnmount
, 那么外部使用的时候给它传递初始化登录注册组件的方法即可:
if(loading) {
return (
<LoadingSpin
size="large"
containerStyle={{
width: '100vw',
height: '100vh'
}}
willUnmountCb={
() => {
//初始化登录注册
}
}
/>
);
}
这样的方式就不会破坏react
的hooks
的顺序, 同时也达到了我们的需求
如果你觉得这篇文章对你有用的话记得给我点个赞, 点个收藏, 众人拾柴, 愿没有难写的代码, 没有难实现的需求
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!