记录改了三个小时才改好的bug问题

101 阅读2分钟

周内工作的时候 接手了一个react老项目,在开发的时候遇见了useCallbackuseState的值无法更新问题,导致无法正确获取数据执行方法,现在看来问题比较简单,自己当时还是过于着急,特此记录

问题描述

  • 使用 useCallback 返回的方法在执行的时候 ,方法内部的 state值一直是不变的,就算对state进行了更新,但是依然是不变的

先看一个简化问题dmeo 进行详细的描述

App.jsx

import { useCallback, useState } from "react";
import "./styles.css";
import Test from "./test";
​
export default function App() {
  const [num, setNum] = useState(1);
  const showNum = useCallback(() => {
    console.log(num);
  }, [num]);
​
  return (
    <div className="App">
      <h1>{num}</h1>
      <button onClick={() => setNum(num + 1)}>增加</button>
      <Test onShow={showNum} />
    </div>
  );
}
​

test.jsx 因为老项目是class组件,所以demo也是class组件

import React from "react";
import "./styles.css";
​
class Test extends React.Component {
​
  scrollFn = (onShow) => {
    const scorllDom = document.getElementById("scorll");
    scorllDom.addEventListener("scroll", onShow);
  };
​
  componentDidMount() {
    const { onShow } = this.props;
​
    this.scrollFn(onShow);
  }
  render() {
    return (
      <div className="test" id="scorll">
        <h1>滚动</h1>
        <div className="big"></div>
      </div>
    );
  }
}
​
export default Test;
​

style.css

.App {
  font-family: sans-serif;
  text-align: center;
}
.test {
  width: 100%;
  height: 200px;
  overflow: auto;
}
.big {
  height: 600px;
  background: blue;
}
​

得到界面为

image.png 滚动区域打印出 app.jsx中的num值,为1

image.png 点击增加app.jsx中得num 值变为 2,然后对滚动区域进行滚动,结果打印出来还是1

image.png

问题原因分析

  • 先看 app.jsx是否有问题

    • showNumuseCallback返回方法,第二参数依赖 为num ,没有问题
  • 再看test.jsx是否有问题

    • onShow为props传递参数,在componentDidMount处作为参数传递给了 监听事件
    • 监听时事件只监听了 mount时的onShow,更新的onShow方法无法传递给监听事件 看来问题就出在这里了

解决方案

  • 主要思路

    • 利用 class 组件的 shouldComponentUpdate取到最新的onShow方法
    • 然后每次更新的时候 重新监听新的事件具体代码如下

test.jsx

import React from "react";
import "./styles.css";
​
class Test extends React.Component {
  cachFn = null;
​
  scrollFn = (onShow) => {
    const scorllDom = document.getElementById("scorll");
    scorllDom.removeEventListener("scroll", this.cachFn);
    this.cachFn = onShow; // 缓存方法方便卸载
    scorllDom.addEventListener("scroll", onShow);
  };
​
  shouldComponentUpdate(nextProps) {
    if (nextProps.onShow !== this.props.onShow) {
      this.scrollFn(nextProps.onShow);
    }
    return true;
  }
  componentDidMount() {
    const { onShow } = this.props;
​
    this.scrollFn(onShow);
  }
  render() {
    return (
      <div className="test" id="scorll">
        <h1>滚动</h1>
        <div className="big"></div>
      </div>
    );
  }
}
​
export default Test;
​

测试如下

image.png

如愿以偿打印出了最新的num

  • 其他解决思路

    • 不修改test.jsx问价的情况下 可以把num值改为闭包引用,监听的时候 虽然onShow方法没变,但是值因为是引用的,所以每次可以拿到最新的值,我最后采用了这个修改方式,因为不用对老文件造成修改 避免了新的问题的产生,需要修改app.jsx文件如下

      import { useCallback, useRef, useState } from "react";
      import "./styles.css";
      import Test from "./test";
      ​
      export default function App() {
        const [num, setNum] = useState(1);
        const numRef = useRef(1);
        const showNum = useCallback(() => {
          console.log(numRef.current); // 使用引用值
        }, [numRef]);
      ​
        return (
          <div className="App">
            <h1>{num}</h1>
            <button
              onClick={() => {
                const newNum = num + 1;
                setNum(newNum);
                numRef.current = newNum;
              }}
            >
              增加
            </button>
            <Test onShow={showNum} num={num} />
          </div>
        );
      }
      
    • 修改test.jsxhooks组,利用useEffect来监听onShow方法来更新事件监听中的方法,这个对原文件造成了破坏性修改,不建议,仅仅作为这次讨论,代码如下

      import React, { useEffect } from "react";
      import "./styles.css";
      ​
      const Test = ({ onShow }) => {
        useEffect(() => {
          const scorllDom = document.getElementById("scorll");
          scorllDom.addEventListener("scroll", onShow);
      ​
          return () => {
            const scorllDom = document.getElementById("scorll");
            scorllDom.removeEventListener("scroll", onShow);
          };
        }, [onShow]);
      ​
        return (
          <div className="test" id="scorll">
            <h1>滚动</h1>
            <div className="big"></div>
          </div>
        );
      };
      ​
      export default Test;
      ​