Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

useEffect - Prevent infinite loop when updating state

I'd like the user to be able to sort a list of todo items. When the users selects an item from a dropdown it will set the sortKey which will create a new version of setSortedTodos, and in turn trigger the useEffect and call setSortedTodos.

The below example works exactly how I want, however eslint is prompting me to add todos to the useEffect dependancy array, and if I do it causes an infinite loop (as you would expect).

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

Live Example:

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>

I'm thinking there has to be a better way of doing this that keeps eslint happy.

like image 305
DanV Avatar asked Feb 03 '23 16:02

DanV


1 Answers

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.

like image 146
Felix Kling Avatar answered Feb 06 '23 08:02

Felix Kling