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:&nbsp;
            <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([&lt;&gt;] 工具栏按钮)更新问题以提供问题的可运行 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:&nbsp;
            <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 中调用上下文函数时防止无限循环

在render()中更新状态时,防止无限循环

从 api 获取时反应 useEffect 无限循环

useEffect 中的无限循环

使用 useEffect 反应 axios 获取请求而不会导致无限循环

尽管依赖项没有变化,useEffect 运行无限循环