编辑代码块以包含来自已接受答案的控制台日志。
我有一个搜索栏组件,我已完全将其精简为最简单的形式:
import React, { useEffect, useRef, useState } from "react";
import axios from "axios";
const resultTypes = ["categories", "submissions", "users"];
const SearchBar = () => {
const [results, setResults] = useState([]);
const [selectedResultType, setSelectedResultType] = useState(resultTypes[0]);
console.log("render --------");
console.log("selectedResultType: " + selectedResultType);
const selectedResultTypeRef = useRef(null);
console.log(
"selectedResultTypeRef.current: " + selectedResultTypeRef.current
);
const fetchResults = async () => {
console.log("fetching...");
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos/3"
);
setResults(response.data);
console.log("setResults: ", response.data);
};
useEffect(() => {
console.log("effect 1 ---------");
selectedResultTypeRef.current = selectedResultType;
console.log(
"selectedResultTypeRef.current: " + selectedResultTypeRef.current
);
}, [selectedResultType]);
useEffect(() => {
console.log("effect 2 ---------");
// No need to debounce fetch when changing result type
fetchResults();
}, [selectedResultTypeRef.current]);
return (
<div className="SearchBar__container">
<div className="SearchBarResults__types">
{resultTypes.map((resultType) => (
<button
key={resultType}
onClick={() => {
console.log("click " + resultType + " -------");
setSelectedResultType(resultType);
}}
>
{resultType}
</button>
))}
</div>
</div>
);
};
export default SearchBar;
当我在 3 个按钮(类别、提交和用户)之间单击时,我希望 selectedResultType
发生变化,然后它会更改 selectedResultTypeRef.current
,以便我可以将其传递到一个记忆化、去抖动的函数中。我删除了所有这些,因为它们不相关,但这就是我需要使用 refs 而不仅仅是组件状态的原因。
这是我的问题:当我调用异步函数并等待设置结果的响应时,似乎 selectedResultTypeRef.current
仅每隔一次单击更改(或被识别为更改)。我尝试了单击这 3 个按钮的不同组合,每次单击都会触发调用 fetchResults
的 Hook 。
奇怪的是,它在错过前一个引用更改的提取后提取了两次。我实在不知道这是为什么。同样令人好奇的是,当我用一个简单的 console.log(response.data)
替换 setResults
行时,它永远不会错过任何一个钩子(Hook)。这是控制台输出:
我怀疑 setResults
更新会以某种方式触发 dom 的重新渲染(即使我在这个简化版本中的任何地方都没有使用 results
状态) ,并且它会干扰 selectedResultTypeRef
被识别为由 useEffect
Hook 捕获的更新。我不知道为什么每次更新都会如此,但我至少验证了其一致的模式。
除此之外,我完全不知所措。我不知道还能尝试什么。
最佳答案
在这里,我正在回答“为什么会发生这种情况?”的问题。而不是“如何构建具有去抖功能的搜索栏?”因为故意省略了细节。
重要的是要记住:
- 从
useState
调用 setter如果数据发生更改,则会导致重新渲染。尽管事实上数据并未被使用。 - 当
useEffect
时,效果会在渲染期间检查其依赖项列表。被称为。 - 那些依赖关系已更改的效果将在渲染阶段之后执行。
现在假设所有内容都已加载。 selectedResultType === "categories"
和selectedResultTypeRef.current === "categories"
.
- 点击
submissions
按钮。setSelectedResultType
被调用并开始重新渲染。 - 检查依赖关系:
selectedResultType
已更改为submissions
所以effect 1
将在渲染后调用。selectedResultTypeRef.current
还没有改变,仍然是"categories"
,所以effect 2
不会被执行。 这就是没有重新获取的原因。 -
effect 1
被执行。现在selectedResultType === "submissions"
和selectedResultTypeRef.current === "submissions"
. - 点击
users
按钮 -> 重新渲染。 - 检查依赖关系:
effect 1
将被调用。selectedResultTypeRef.current
已从"categories"
更改为至"submissions"
所以effect 2
也会被执行。 -
effect 1
被执行。现在selectedResultType === "users"
和selectedResultTypeRef.current === "users"
. -
effect 2
被执行 -> 重新获取 ->setResults
-> 重新渲染。 - 检查依赖关系:
effect 1
不会被调用。但是effect 2
会被调用,因为selectedResultTypeRef.current
已从"submissions"
更改为至"users"
. -
effect 2
被执行 -> 重新获取 ->setResults
-> 重新渲染。 - 检查依赖关系:没有变化。
因此,发生的事情是可以理解的,但很难理解。
结论非常简单 - 相信 linter。这会让你省去不少头痛。
Mutable values like 'selectedResultTypeRef.current' aren't valid dependencies because mutating them doesn't re-render the component.
关于javascript - 第一个更新第二个依赖项的相关 useEffects 会导致奇怪的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73510367/