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

时间:2019-11-25 08:43:24

标签: javascript reactjs react-hooks eslint

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

下面的示例完全按照我想要的方式工作,但是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感到高兴。

3 个答案:

答案 0 :(得分:7)

我认为这并不理想。该函数确实取决于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

实时示例:

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>

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

答案 1 :(得分: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 :(得分: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]);

Working codesandbox

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

使用功能性setState可确保您获得状态的最新值,并且不会更改原始状态值。