使用hook时,把携带setState的方法通过props传给子类组件,调用不生效?

一、问题来源

为了学习React的HOOK,我实现了一个简单的计数器,但是把动态修改计数器count的方法通过props传递给子组件时,出现了只生效一次,再次调用不生效的问题。

二、场景介绍

父组件是一个函数式组件,使用了useState来动态更新Count。子组件是类组件,通过父组件传递的回调函数props.changeCount来修改父组件的count。但是出现了调用changeCount不生效的奇怪现象。下面是出现问题的代码,大家可以先分析下问题出在了哪里。

import React,{useState} from 'react';

class FancyButton extends React.Component {
  handleClick = this.props.changeCount
  render() {
      return (
          <button onClick={this.handleClick}>
              {this.props.label}
          </button>
      )
  }
}

function App() {
  const [count, setCount] = useState(0);
  const changeCount=()=>{
    setCount(count + 1 )
  } 
  return (
    <div>
      <span>{count}</span>
      <FancyButton
        label="+"
        changeCount={changeCount}
      />
    </div>
  );
}
export default App;

相信熟悉React函数式组件和类组件工作原理的人一眼就看出来问题所在。
没错是因为我手贱,把this.props.handleClick赋值给this.handleClick,才导致点击button后,计数器无法更新的情况。


为什么出现上述情况呢?

我如果将FancyButton这个组件做一个简单的改造,可能会更直观一点。

class FancyButton extends React.Component {
  constructor(props){
  	super(props);
  	//其实这段代码和上面的代码是一样的效果,只不过语法上的改变
  	//但是这样写就很容易找到问题的所在
  	this.handleClick = this.props.changeCount;
  }
  handleClick = this.props.changeCount
  render() {
      return (
          <button onClick={this.handleClick}>
              {this.props.label}
          </button>
      )
  }
}

这样简单改造后,我们可能就能之间发现问题所在,下面先介绍为什么第一次点击的时候会成功,然后再说明为什么再次点击的时候会失败。

  • 为什么第一次点击的时候会成功?
    学习过react声明周期函数的同学肯定知道constructor函数只会在组件第一次初始化的时候调用一次,所以整个程序初始化的时候我们把父组件的changeCount函数赋值给FancyButtonhandleClick进行绑定,所以第一次点击按钮的时候一定会成功。
  • 为什么再次点击的时候无效?
    现在我们把关注点放在第一次点击后发生了什么react内部发生了什么事情,首先点击后间接触发了setCount()函数修改了Count的值,此时App组件中的 state发生了改变导致重新渲染,其实就是再从头到位执行一遍App这个函数,那么意味着App再次执行前后changeCount函数不是一个函数了,并且每次重新执行count的值仍然是1 由于React性能优化的机制,自组件并不能监测到props.changCount发生了改变,所以此时组件FancyButton中的this.handleClick引用的仍然是第一次constructor初始化式绑定的changeCount,这样不仅会造成this.handleClick失去了绑定,而且还造成了内存泄漏😂;

解决方案

  1. 采用函数式更新state
    仍然在构造函数中绑定changeCount,修改App组件中的changeCount函数,采用函数式更新state,这样做的原因利用Hook闭包的原理,每次更新调用的setCount函数中传递的prev更新Count,而这prev是从hook的闭包环境中获取的值,所以每次点击都会改变。
    function App() {
      const [count, setCount] = useState(0);
      const changeCount=()=>{
      	console.log(count);// 0
      	//采用函数式更新
        setCount(prev=>prev+1)
      } 
      return (
        <div>
          <span>{count}</span>
          <FancyButton
            label="+"
            changeCount={changeCount}
          />
        </div>
      );
    }
    
  2. 在声明周期函数中修改绑定,以render为例
    FancyButton组件中绑定changeCount,这么做的原因是每次父组件更新会导致更新子组件的render重新执行,在这里做重新绑定也可以达到预期效果
    class FancyButton extends React.Component {
      render() {
          //在此处进行赋值绑定
     	  this.handleClick = this.props.changeCount
          return (
              <button onClick={this.handleClick}>
                  {this.props.label}
              </button>
          )
      }
    }
    
  3. 惯用的写法(闭包)
    这样做是constructor执行是生成一个箭头函数保存对this.props.changeCount()这个方法的引用,实质上是利用闭包的原理,所以每次调用相当于在App调用changeCount()这函数。
    class FancyButton extends React.Component {
      //这样做实质上是引用了App中的值(函数也是值),产生了闭包
      handleClick =()=>this.props.changeCount()
      render() {
          return (
              <button 
              	onClick={this.handleClick}
              	//如果不在上面使用handleClick =()=>this.props.changeCount()
              	//在此直接使用onClick={this.props.changeCount}
              	//原理仍然是闭包,请自行理解
              >
                  {this.props.label}
              </button>
          )
      }
    }
    function App() {
      const [count, setCount] = useState(0);
      const changeCount=()=>{
      	//每次打印都是0
      	console.log(count);// 0
        setCount(count + 1)
      } 
      return (
        <div>
          <span>{count}</span>
          <FancyButton
            label="+"
            changeCount={changeCount}
          />
        </div>
      );
    }
    
    上面这种做法实质上是下面做法闭包的实现,下面是HOOK比较正常的使用方式,参见React HOOK 规则
    function App() {
      const [count, setCount] = useState(0);
      const changeCount=()=>{
      	//每次结果都是期望的结果0,1,2...
      	//至于为什么,请去阅读一下官方文档HOOK规则,此处不再赘述
      	console.log(count);// 0->1->2...
      	//注意此处还是赋值式更新
        setCount(count + 1)
      } 
      return (
        <div>
          <span>{count}</span>
          <button
            onClick={changeCount}
          >
          	+
          </button>
        </div>
      );
    }
    

以上就是比较常见的三种解决方案。

未来

如果对此问题仍然存有疑惑,个人觉得应当阅读一下源码,我也会在未来重新用源码实现的角度,重新描述这个问题出现的原因,以及解决的思路。
另外,如果有问题,或者发现文章中的错误欢迎留言指正,谢谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值