ユーザーがToDoアイテムのリストを並べ替えられるようにしたい。ユーザーがドロップダウンからアイテムを選択すると、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:
<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:
<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>
「ベース」配列とソートキーから常にソートされた配列を導出/計算できるため、ソートされた値を状態に保存する必要はありません。また、コードの複雑さが軽減されるため、コードを理解しやすくなります。
無限ループの理由は、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は正しいです!上記のケースでは、Todoは依存関係であり、リストに含まれている必要があります。アイテムの選択に関するアプローチが間違っているため、問題が発生しています。
ここで必要なのは、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
を使用すると、状態の最新の値を取得し、元の状態の値を変更しないことが保証されます。