【问题标题】:useEffect - Prevent infinite loop when updating stateuseEffect - 更新状态时防止无限循环
【发布时间】:2023-03-22 09:48:02
【问题描述】:

我希望用户能够对待办事项列表进行排序。当用户从下拉列表中选择一个项目时,它将设置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 上的依赖数组中,并且您可以看到为什么不应该这样做。 :-)
  • 我为您添加了实时示例。真的很想看到这个答案。

标签: javascript reactjs react-hooks eslint


【解决方案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 的代码,并没有注意这一点(与问题无关)。
  • 真正简单易懂的解决方案。
  • 啊,是的,使用备忘录!忘记了:)
猜你喜欢
  • 2019-11-11
  • 2020-01-09
  • 1970-01-01
  • 1970-01-01
  • 2021-10-21
  • 1970-01-01
  • 1970-01-01
  • 2020-09-06
  • 1970-01-01
相关资源
最近更新 更多