Little React Things: Cleaning up dependencies

We're going to look at a simple way to trim some unnecessary dependencies from our dependency lists.

Let's kick this short one off with a little thing you may or may not know, but you definitely should know. The setState function identity does not change on rerenders. What this means is that you can (and should) omit setStates from the dependency lists of useEffect and useCallback. This behavior is noted in the official React docs.

On to the main topic. For this post I'm gonna roll with the tried and true Todo List example. We have a list of TodoItems and let's say we can have a bunch of them. Since there can be so many of them we wrap each TodoItem with memo so that each one only rerenders when its own props change.

Furthermore, each todo item can be cloned, so each TodoItem has a callback, onClone. And we have a little helper function cloneTodo that makes such a clone of an existing todo item. The cloned todo is appended to the todos state in handleClone (if you want to know why I wrapped handleClone with useCallback take a look at Your Guide to React.useCallback()).

Here's our React component:

function TodoList() {
    const [todos, setTodos] = useState([])

    const handleClone = useCallback((todo) => {
        setTodos([...todos, cloneTodo(todo)])
    }, [todos])
    
    return (
        <div>
            {todos.map(todo => (
                <TodoItem key={todo.id} todo={todo} onClone={handleClone} />
            ))}
        </div>
    )
}

Pretty reasonable right? Ship it!

...a few minutes later...

Uh oh, users have started reporting that their todo lists are really slow, particularly when cloning todo items. What happened? We wrapped our TodoItems with memo, so why are they all rendering? Well, one of the props of TodoItem is onClone and we used useCallback for that. And... oh, one of the dependencies of handleClone is todos. And that todos state changes every time we clone a todo (because we add a new todo to the array). So cloning a todo rerenders all the todos. Not great.

Let's make a small change to handleClone:

const handleClone = useCallback((todo) => {
    setTodos(currentTodos => [...currentTodos, cloneTodo(todo)])
}, [])

As I mentioned at the beginning, setTodos does not need to be added to the dependency list. So now this new version of handleClone doesn't have any dependencies and its identity remains stable throughout the lifetime of the component! Now whenever we clone a todo, that new todo is added to the list without rerendering the already existing ones.

If you want to see these examples live, check them out here: