useEffect-更新状态时防止无限循环


9

我希望用户能够对待办事项列表进行排序。当用户从下拉列表中选择一项时,它将设置,sortKey这将创建的新版本setSortedTodos,并依次触发useEffectand调用setSortedTodos

下面的示例完全按照我想要的方式工作,但是eslint提示我将其添加todosuseEffect依赖项数组,如果执行此操作,则会导致无限循环(正如您期望的那样)。

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]);

现场示例:

我认为必须有一种更好的方法来使eslint感到高兴。


1
附带说明:sort回调可以是:return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());如果环境具有合理的语言环境信息,则它还具有进行语言环境比较的优势。如果您愿意,也可以对其进行破坏性处理:pastebin.com/7X4M1XTH
TJ Crowder

eslint抛出什么错误?
Luze

您能否使用Stack Snippets(工具栏按钮)更新问题以提供可运行的最小重现性示例[<>]?堆栈片段支持React,包括JSX;这是一种方法。这样,人们可以检查他们提出的解决方案是否没有无限循环问题……
TJ Crowder

这是一个非常有趣的方法,一个非常有趣的问题。如您所说,您可以理解为什么ESLint认为您需要添加todos到上的依赖项数组useEffect,并且可以了解为什么不应该这样做。:-)
TJ Crowder

我为您添加了实时示例。真的很想看到这个答案。
TJ Crowder

Answers:


8

我认为这意味着这样做并不理想。功能确实取决于todos。如果setTodos在其他地方调用,则必须重新计算回调函数,否则它将对陈旧数据进行操作。

您为什么仍然将排序后的数组存储在状态中?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到处引用。

现场示例:

无需将排序后的值存储在状态中,因为您始终可以从“基本”数组和排序键中派生/计算排序后的数组。我认为这也使您的代码更容易理解,因为它不那么复杂。


哦,很好用useMemo。只是一个侧面问题,为什么不使用.localCompare排序呢?
迪克斯

2
那确实会更好。我只是复制了OP的代码,却没有注意(与问题无关)。
菲利克斯·克林

真正简单,易于理解的解决方案。
TJ Crowder

嗯,useMemo!忘记了:)
DanV

3

无限循环的原因是,待办事项与先前的参考不匹配,并且效果将重新运行。

为什么仍要为点击动作使用效果?您可以按以下功能运行它:

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是正确的!在上述情况下,您的待办事项是依赖项,应该在列表中。选择项目的方法是错误的,因此是您的问题。


2
“ sort将返回数组的新实例”不是内置的sort方法。它对数组进行原位排序。data.slice(0)创建副本。
菲利克斯·克林

当您执行操作a时,这是不正确的,setState因为它不会编辑现有对象,因此会在内部对其进行克隆。我的回答中的措词有误,是的。我将对此进行编辑。
迪克斯

setState不克隆数据。为什么你这么想?
菲利克斯·克林

1
@Tikkes-不,setState不克隆任何内容。Felix和OP是正确的,您需要在对数组进行排序之前先对其进行复制。
TJ Crowder

好的,对不起。我似乎需要更多有关内部的阅读。您确实必须复制而不更改现有的state
迪克斯

0

您需要在这里使用的功能形式是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]);

工作代码和邮箱

即使您为了不改变原始状态而复制状态,由于将状态设置为异步状态,仍不能保证会获得其最新值。另外,大多数方法都将返回浅表副本,因此无论如何您可能最终都会改变原始状态。

使用函数setState可确保您获取状态的最新值,并且不会更改原始状态值。


“并且请勿更改原始状态值。” 我不认为React将副本传递给回调。.sort就地更改数组,因此您仍然必须自己复制它。
菲利克斯·克林

嗯,好点,我实际上找不到任何确认它通过状态副本的确认,尽管我记得以前读过类似的文章。
清晰度

在这里找到它:reactjs.org/docs/…。'我们可以使用setState的功能更新形式。它使我们可以指定状态如何更改而无需引用当前状态 '
Clarity

我不认为这是获得副本。这只是意味着您不必提供当前状态作为“外部”依赖项。
菲利克斯·克林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.