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



我希望用户能够对待办事项列表进行排序。当用户从下拉列表中选择一个项目时,它将设置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 开心。

我认为这意味着以这种方式进行并不理想。该功能确实依赖于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>

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

无限循环的原因是因为 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 是对的! 在上述情况下,您的待办事项是一个依赖项,应该在列表中。 选择项目的方法错误,因此您的问题。

这里你需要做的是使用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可确保获取状态的最新值,并且不会更改原始状态值。

相关内容

  • 没有找到相关文章

最新更新