Little React Things is a series of short, React-focused posts where I share some things I've learned over the past few years of developing with React. I hope you find them helpful. These posts might be most helpful for those who have at least a little experience with React already.
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 setState
s 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 TodoItem
s 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 TodoItem
s 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 everytime 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:
Top comments (0)