useEffect - 更新状态时防止无限循环
Posted
技术标签:
【中文标题】useEffect - 更新状态时防止无限循环【英文标题】:useEffect - Prevent infinite loop when updating state 【发布时间】:2020-03-20 12:21:01 【问题描述】:我希望用户能够对待办事项列表进行排序。当用户从下拉列表中选择一个项目时,它将设置sortKey
,这将创建一个新版本的setSortedTodos
,然后触发useEffect
并调用setSortedTodos
。
下面的例子完全符合我的要求,但是 eslint 提示我将 todos
添加到 useEffect
依赖数组,如果我这样做会导致无限循环(如您所料)。
const [todos, setTodos] = useState([]);
const [sortKey, setSortKey] = useState('title');
const setSortedTodos = useCallback((data) =>
const cloned = data.slice(0);
const sorted = cloned.sort((a, b) =>
const v1 = a[sortKey].toLowerCase();
const v2 = b[sortKey].toLowerCase();
if (v1 < v2)
return -1;
if (v1 > v2)
return 1;
return 0;
);
setTodos(sorted);
, [sortKey]);
useEffect(() =>
setSortedTodos(todos);
, [setSortedTodos]);
现场示例:
const useState, useCallback, useEffect = React;
const exampleToDos = [
title: "This", priority: "1 - high", text: "Do this",
title: "That", priority: "1 - high", text: "Do that",
title: "The Other", priority: "2 - medium", text: "Do the other",
];
function Example()
const [todos, setTodos] = useState(exampleToDos);
const [sortKey, setSortKey] = useState('title');
const setSortedTodos = useCallback((data) =>
const cloned = data.slice(0);
const sorted = cloned.sort((a, b) =>
const v1 = a[sortKey].toLowerCase();
const v2 = b[sortKey].toLowerCase();
if (v1 < v2)
return -1;
if (v1 > v2)
return 1;
return 0;
);
setTodos(sorted);
, [sortKey]);
useEffect(() =>
setSortedTodos(todos);
, [setSortedTodos]);
const sortByChange = useCallback(e =>
setSortKey(e.target.value);
);
return (
<div>
Sort by:
<select onChange=sortByChange>
<option selected=sortKey === "title" value="title">Title</option>
<option selected=sortKey === "priority" value="priority">Priority</option>
</select>
todos.map((text, title, priority) => (
<div className="todo">
<h4>title <span className="priority">priority</span></h4>
<div>text</div>
</div>
))
</div>
);
ReactDOM.render(<Example />, document.getElementById("root"));
body
font-family: sans-serif;
.todo
border: 1px solid #eee;
padding: 2px;
margin: 4px;
.todo h4
margin: 2px;
.priority
float: right;
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
我认为必须有更好的方法来让 eslint 开心。
【问题讨论】:
附带说明:sort
回调可以是:return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());
,如果环境具有合理的区域信息,它还具有进行区域比较的优势。如果你愿意,你也可以对它进行解构:pastebin.com/7X4M1XTH
eslint
抛出什么错误?
您能否使用 Stack Snippets([<>]
工具栏按钮)更新问题以提供问题的可运行 minimal reproducible example? Stack Snippets 支持 React,包括 JSX; here's how to do one。这样人们就可以检查他们提出的解决方案是否存在无限循环问题......
这是一个非常有趣的方法,也是一个非常有趣的问题。正如您所说,您可以理解为什么 ESLint 认为您需要将 todos
添加到 useEffect
上的依赖数组中,并且您可以看到为什么不应该这样做。 :-)
我为您添加了实时示例。真的很想看到这个答案。
【参考方案1】:
死循环的原因是因为todos与之前的引用不匹配,效果会重新运行。
为什么还要对点击动作使用效果? 你可以在这样的函数中运行它:
const [todos, setTodos] = useState([]);
function sortTodos(e)
const sortKey = e.target.value;
const clonedTodos = [...todos];
const sorted = clonedTodos.sort((a, b) =>
return a[sortKey.toLowerCase()].localeCompare(b[sortKey.toLowerCase()]);
);
setTodos(sorted);
然后在您的下拉列表中输入onChange
。
<select onChange="sortTodos"> ......
顺便注意一下依赖,ESLint 是对的! 在上述情况下,您的待办事项是一个依赖项,应该在列表中。 选择项目的方法是错误的,因此是您的问题。
【讨论】:
“排序将返回一个新的数组实例” 不是内置的排序方法。它就地对数组进行排序。data.slice(0)
创建副本。
当您执行setState
时,情况并非如此,因为它不会编辑现有对象,因此会在内部克隆它。我的回答中的措辞错误,是的。我会编辑这个。
setState
不克隆数据。你为什么这么认为?
@Tikkes - 不,setState
不会克隆任何东西。 Felix和OP是正确的,你需要在排序之前复制数组。
好吧,对不起。我似乎需要更多关于内部的阅读。您确实必须复制而不是更改现有的state
。【参考方案2】:
这里你需要做的是使用setState
的函数形式:
const [todos, setTodos] = useState(exampleToDos);
const [sortKey, setSortKey] = useState('title');
const setSortedTodos = useCallback((data) =>
setTodos(currTodos =>
return currTodos.sort((a, b) =>
const v1 = a[sortKey].toLowerCase();
const v2 = b[sortKey].toLowerCase();
if (v1 < v2)
return -1;
if (v1 > v2)
return 1;
return 0;
);
)
, [sortKey]);
useEffect(() =>
setSortedTodos(todos);
, [setSortedTodos, todos]);
Working codesandbox
即使您为了不改变原始状态而复制状态,由于将状态设置为异步,仍然不能保证您会获得其最新值。另外,大多数方法都会返回一个浅拷贝,所以你最终可能会改变原始状态。
使用函数式setState
可确保您获得最新的状态值,并且不会改变原始状态值。
【讨论】:
"并且不要改变原始状态值。" 我认为 React 不会将 copy 传递给回调。.sort
就地更改数组,因此您仍然需要自己复制它。
嗯,好点,我实际上找不到任何确认它通过了状态副本,尽管我记得之前读过类似的东西。
在这里找到它:reactjs.org/docs/…。 '我们可以使用setState的函数更新形式。它让我们指定状态需要如何改变不参考当前状态'
我不认为这是获得副本。这只是意味着您不必将当前状态作为“外部”依赖项提供。【参考方案3】:
我认为这意味着以这种方式进行并不理想。该功能确实依赖于todos
。如果setTodos
在其他地方被调用,回调函数必须重新计算,否则它对陈旧数据进行操作。
为什么还要将排序后的数组存储在 state 中?当键或数组发生变化时,您可以使用useMemo
对值进行排序:
const sortedTodos = useMemo(() =>
return Array.from(todos).sort((a, b) =>
const v1 = a[sortKey].toLowerCase();
const v2 = b[sortKey].toLowerCase();
if (v1 < v2)
return -1;
if (v1 > v2)
return 1;
return 0;
);
, [sortKey, todos]);
然后到处引用sortedTodos
。
现场示例:
const useState, useCallback, useMemo = React;
const exampleToDos = [
title: "This", priority: "1 - high", text: "Do this",
title: "That", priority: "1 - high", text: "Do that",
title: "The Other", priority: "2 - medium", text: "Do the other",
];
function Example()
const [sortKey, setSortKey] = useState('title');
const [todos, setTodos] = useState(exampleToDos);
const sortedTodos = useMemo(() =>
return Array.from(todos).sort((a, b) =>
const v1 = a[sortKey].toLowerCase();
const v2 = b[sortKey].toLowerCase();
if (v1 < v2)
return -1;
if (v1 > v2)
return 1;
return 0;
);
, [sortKey, todos]);
const sortByChange = useCallback(e =>
setSortKey(e.target.value);
, []);
return (
<div>
Sort by:
<select onChange=sortByChange>
<option selected=sortKey === "title" value="title">Title</option>
<option selected=sortKey === "priority" value="priority">Priority</option>
</select>
sortedTodos.map((text, title, priority) => (
<div className="todo">
<h4>title <span className="priority">priority</span></h4>
<div>text</div>
</div>
))
</div>
);
ReactDOM.render(<Example />, document.getElementById("root"));
body
font-family: sans-serif;
.todo
border: 1px solid #eee;
padding: 2px;
margin: 4px;
.todo h4
margin: 2px;
.priority
float: right;
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
没有必要将排序后的值存储在状态中,因为您始终可以从“基本”数组和排序键中派生/计算排序后的数组。我认为它还使您的代码更容易理解,因为它不那么复杂。
【讨论】:
哦,很好地使用了useMemo
。只是一个附带问题,为什么不在排序中使用.localCompare
?
那确实会更好。我只是复制了 OP 的代码,并没有注意这一点(与问题无关)。
真正简单易懂的解决方案。
啊,是的,使用备忘录!忘记了:)以上是关于useEffect - 更新状态时防止无限循环的主要内容,如果未能解决你的问题,请参考以下文章
React:在 useEffect 中调用上下文函数时防止无限循环