useEffect - Prevent infinite loop when updating state

I'd argue that this means that going about it this way is not ideal. The function is indeed dependent on todos. If setTodos is called somewhere else, the callback function has to be recomputed, otherwise it operates on stale data.

Why do you store the sorted array in state anyway? You can use useMemo to sort the values when either the key or the array changes:

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

Then reference sortedTodos everywhere.

Live Example:

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>

There is no need to store the sorted values in state, since you can always derive/compute the sorted array from the "base" array and the sort key. I'd argue it also makes your code easier to understand since it is less complex.


The reason for the infinite loop is because todos doesn't match the previous reference and the effect will rerun.

Why use an effect for a click-action anyway? Your can run it in a function like so:

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

and on your dropdown do an onChange.

    <select onChange="sortTodos"> ......

Note on the dependency by the way, ESLint is right! Your Todos, in the case described above, are a dependency and should be in the list. The approach on the selection of an item is wrong, and hence your problem.