乐趣区

茴字的四种写法如何在React-Hook中获得最新的state

今天的这个问题也源于 生活(工作)????。在我们刚开始使用 React hook 的时候,经常会遇到这样的情况:我需要在某个异步请求 / 事件监听中更新我的 state 的值,并拿着更新好的 state 去做什么事情。这个时候有可能就会遇到这样的情况,state 的值并没有更新,我们拿到的总是旧的 state。为什么会有这种情况?我们有哪些方法可以来解决它?本文将会带你解决这些问题。

1. 问题重现

首先来个 demo,如下:

import React from "react";
import "./styles.css";
const {useState, useEffect} = React;
export default function App() {const [scrollCount, setScrolledCount] = useState(0);
  useEffect(() => {console.log(`useEffect: ${scrollCount}`);
  });

  const handleScroll = () => {console.log(`handleScroll: ${scrollCount}`);
    setScrolledCount(scrollCount + 1);
  };

  useEffect(() => {document.addEventListener("scroll", handleScroll);
    return () => document.removeEventListener("scroll", handleScroll);
  // ⚠️注意:这里是空数组,代表了只有在组件挂载时运行一次
  }, []);
  return (
    <div className="App">
      <h1>React hook 获得最新 state Demo</h1>
    </div>
  );
}

在这个例子中,我们监听了滚动事件,然后持续的将 scrollCount 加 1,同时持续输出 scrollCount 的值。毫无疑问,我们预想的结果应该是:

useEffect: 0
handleScroll: 0
useEffect: 1
handleScroll: 1
useEffect: 2
handleScroll: 2
...

但是实际的结果是:

useEffect: 0
handleScroll: 0
useEffect: 1
handleScroll: 0
// 因为 state 的值没有被更新,所以组件没有被重新渲染,useEffect 也没有运行
handleScroll: 0
handleScroll: 0
...

那么 为什么会出现这样的拿不到最新的 state 的情况呢?

因为 useEffect 执行时,会创建一个闭包,所以在运行的时候的 scrollCount 的值被保存在这个闭包中,且初始值为 0,所以当滚动时,每次都输出 0

❗️React hook 相关原理可以参照:《React Hook 原理》——这是我觉得讲的最清楚的一篇,没有之一。

那怎么样解决这个问题呢?

2. 四种解法

2.1 useEffect 去掉依赖的空数组

这种解法是最简单的,去掉空数组即可。这样,每次 useEffect 都会运行,尽管还是个闭包,但是每次都拿了最新的 scrollCount 值,所以 handleScroll 的输出也会持续更新。

有些同学可能会问:事件被反复被加绑和解绑没有问题吗?

没有问题,由于 useEffect 会在浏览器完成布局与绘制 之后 调用,且加绑和解绑事件的性能开销很小,所以这并不是问题。

⚠️注意:记得解绑事件,持续的添加事件绑定肯定会有问题

  useEffect(() => {document.addEventListener("scroll", handleScroll);
    return () => document.removeEventListener("scroll", handleScroll);
  // ⚠️改动了这里:去掉了 useEffect 的第二个参数,空数组
  });

2.2 将函数移入到 useEffect 之内,并添加依赖的 state

因为 useEffect 依赖了 scrollCount,所以每次 scrollCount 的改动都会使得 useEffect 重新运行,从而获得了当前最新的 scrollCount。

当然了,需要强调的是,函数移不移入 useEffect 之内其实并没有影响,只不过移入了之后,你会更容易的看到到底函数中依赖了哪个 state。

  useEffect(() => {
    // ⚠️改动了这里:handleScroll 被移入了 useEffect 内部
    const handleScroll = () => {console.log(`handleScroll: ${scrollCount}`);
      setScrolledCount(scrollCount + 1);
    };
    
    document.addEventListener("scroll", handleScroll);
    return () => document.removeEventListener("scroll", handleScroll);
  // ⚠️改动了这里:空数组变为[scrollCount]
  }, [scrollCount]);

2.3 使用 setState 的函数式更新

函数式更新:如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。来自 React 官方文档

  const handleScroll = () => {console.log(`handleScroll: ${scrollCount}`);
    // ⚠️改动了这里:表达式变成了函数
    setScrolledCount(scrollCount => scrollCount + 1);
  };

通过使用这个特性,我们可以保证 state 每次都被更新了,但是 handleScroll 中获得的 scrollCount 值还是闭包中的 0。我们实际得到的输出如下:

useEffect: 0
handleScroll: 0
useEffect: 1
handleScroll: 0
useEffect: 2
handleScroll: 0
...

2.4 使用全局变量

回顾这个问题,无非是某个状态不能获得最新的,我们使用全局变量就能解决这个问题,无论是把这个 state 挂载到 window 下,还是使用 useRef,都能获得最新的值。

这种改动代码变化稍大,整体贴在下面:

import React from "react";
import "./styles.css";
const {useRef, useEffect} = React;
export default function App() {const scrollCountRef = useRef(null);

  useEffect(() => {console.log(`useEffect: ${scrollCountRef.current}`);
  });

  const handleScroll = () => {console.log(`handleScroll: ${scrollCountRef.current}`);
    scrollCountRef.current++;
  };

  useEffect(() => {
    scrollCountRef.current = 0;
    document.addEventListener("scroll", handleScroll);
    return () => document.removeEventListener("scroll", handleScroll);
  }, []);
  return (
    <div className="App">
      <h1>React hook 获得最新 state Demo</h1>
    </div>
  );
}

当然,这种代码也不涉及到组件的重新渲染,所以它的输出是:

useEffect: null
handleScroll: 1
handleScroll: 2
handleScroll: 3
...

3. 总结

在本文中,我们探讨了问题发生的原因和 4 种解决 React Hook 种获得最新 state 的办法。问题本身并不复杂,但是深入了解其中的原理,并不断进行总结,才是我们持续不断进步的源泉。

退出移动版