最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • react hooks报错: Rendered fewer hooks/Rendered more hooks

    正文概述 掘金(_音魂不散_)   2021-04-06   498

    这是在项目开发过程中使用reactfunction componenthooks遇到的问题, 平时在写代码的过程中, 整个页面级别的文件我会选择使用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 hooks报错: Rendered fewer hooks/Rendered more hooks

    当然了, 我只是举个例子, 毕竟我们用react进行开发, 这么样的一个需求, 无论如何也不会这么写, 毕竟任何技能你熟练到一定程度, 想犯错反而是个比较困难的事, 这不是凡尔赛哈, 我想写这个报错的例子也还是想了一下才写出来的...

    咳...咳...言归正传, 此时我们修改gender, 那么页面就会报错了:

    react检测到了Vero调用hook的顺序发生了改变, 如果不修复将会导致一些问题和错误, 详情请查阅hooks的规则

    比上一次多渲染了些hooks

    那么我来看一看, 到底怎么就比上一次多渲染了, 首先, 程序渲染的是3hooks:

      const [ gender, setGender ] = useState('male');
      const [ isUnder18, setIsUnder18 ] = useState(0);
      const [ isOver60, setIsOver60 ] = useState(0);
    

    此时由于if判断, 最终我们的函数式组件返回的jsx渲染结果为:

    18岁以下
    60岁以上
    性别
    

    然后我们修改了性别state: gender, 将它的值从初始值male修改为了female, 那么此时Vero函数重新执行, 最终渲染结果为: 4hooks:

      const [ gender, setGender ] = useState('male');
      const [ isUnder18, setIsUnder18 ] = useState(0);
      const [ isOver60, setIsOver60 ] = useState(0);
      const [ gestation, setGestation ] = useState(1);
    

    if条件不再满足, 不执行, 渲染结果为:

    18岁以下
    60岁以上
    性别
    妊娠期
    

    但程序报错, 最终的渲染结果没能正常显示出来, 此时我们发现:

    上一次渲染了3hooks:

      const [ gender, setGender ] = useState('male');
      const [ isUnder18, setIsUnder18 ] = useState(0);
      const [ isOver60, setIsOver60 ] = useState(0);
    

    接下来渲染了4hooks:

      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能在多个useStateuseEffect的调用中正确保持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 renderPrevious render多渲染了1hook:

      const [ gestation, setGestation ] = useState(1);
    

    在报错的结果中其实也有显示这两次的渲染差异:

    react hooks报错: Rendered fewer hooks/Rendered more hooks

    我们的代码导致了前后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')
    

    react hooks报错: Rendered fewer hooks/Rendered more hooks

    同样, 修改我们的性别这个state, 此时react会给我们报如下的错误:

    Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

    为何这回是fewer而不是more了呢?我们根据上面的知识来看看, 首先Previous render, 因为if的条件修改了之后if里面的语句无法被执行, 直接渲染出了带妊娠期jsx, 也就是此次渲染渲染了4hooks:

      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的位置, 因此这回只渲染了3hooks, 也就是顶部的那3hooks:

      const [ gender, setGender ] = useState('male');
      const [ isUnder18, setIsUnder18 ] = useState(0);
      const [ isOver60, setIsOver60 ] = useState(0);
    

    我们可以自己写一下Previous renderNext render, 这样会有一个更加直观的比较:

    Previous renderNext render
    gendergenderisUnder18isUnder18isOver60isOver60gestationundefined

    这样就能直观的看到前后两次渲染中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;
    

    性别 statemale(也就是初次渲染)时:

    react hooks报错: Rendered fewer hooks/Rendered more hooks

    当我们修改了性别 statefemale时:

    react hooks报错: Rendered fewer hooks/Rendered more hooks

    然后就可以愉快地去打疫苗了

    在项目中的场景

    在自己负责的一个项目中, 网络请求使用了graphql(apollo-client), 同时是ssr(nextjs)的项目, 里面还需要获取用户的地理位置, 这里使用了百度地图JavaScript API v3.0类参考_Geolocation, 这里顺便吐槽一下百度地图是真难用, 之前用过百度云存储, 文档不全, 连蒙带猜的使用, 功能提供不全, 还要去改源码, 醉了, 现在写文章的时候发现之前的定位demo找不到了, 就附上Geolocation这个类的文档

    获取用户位置的操作是异步的, 以及我们需要在程序渲染到了客户端也就是浏览器的时候根据浏览器自带的定位接口定位, 失败了再走IP定位, 这个逻辑百度定位API帮我们封装好了, 我们直接使用即可, 但因为是异步的, 我们需要这么做, 流程如下:

    1. 先呈递一版页面给用户
    2. 获取用户坐标
    3. 在拿到定位之后请求后端接口重新获取数据
    4. 页面loading
    5. 请求结束取消loading
    6. 用新的数据重新渲染页面

    页面使用了function component, 同时使用了hooks, 项目一直都很ok, 直到接入登录注册流程, 登录注册是另一个小伙伴写的, 他封装成了一个js文件, 也就是说这个jswindow上加了一个object, 使用传统的方式, 就是传递登录注册框divid的方式来使用, 我需要在程序渲染到浏览器的时候去访问window来拿到他封装的这个object从而初始化登录注册功能

    查看上面的流程, 我需要在第6步之后做这个动作, 因为如果在获取坐标之前做这个操作, 如果用户同意获取坐标, 此时拿到了坐标重新渲染, 那么初始化之后的登录注册框将消失, 于是只能在第二次渲染页面也就是第6步之后来做:

    if(loading) {
    
      return <LoadingSpin />;
    }
    
    useEffect(
      () => {
        //初始化登录注册框
      },
      []
    )
    

    这个时候就犯了上面的demo中的错了, 我在条件判断return之后使用了hook, 此时调用顺序会发生变化, 最终导致react报错, 但我如果将useEffect则会在获取到用户坐标的时候页面重新渲染而导致登录注册初始化失效, 此时我想到了一个方法: 既然获取到坐标再去请求数据的时候要渲染loading组件, 那么我在loadingcomponentWillUnmount生命周期中做初始化操作不就行了

    说干就干:

    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={
              () => {
                //初始化登录注册
              }
            }
          />
        );
      }
    

    这样的方式就不会破坏reacthooks的顺序, 同时也达到了我们的需求

    如果你觉得这篇文章对你有用的话记得给我点个赞, 点个收藏, 众人拾柴, 愿没有难写的代码, 没有难实现的需求


    起源地下载网 » react hooks报错: Rendered fewer hooks/Rendered more hooks

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元