In order to detect what to rerender, immutable patterns can be used. If the view
is a function of the state; and the state is immutable; then we can do a simple
===
(referential equality) comparison to determine if a rerender is needed.
If something is immutable it means it cannot be changed. For JavaScript, this
means you are not allowed to modify an existing object. Instead, you must create
a new object and use that going forward. While the new object has not yet
replaced the existing one, you may still modify it. Then, you can copy some
existing properties into the new object. After you finish creating the new value
of the object, you replace the existing one.
React takes advantage of immutability when checking if a rerender is needed.
Whenever you call a React state setter function, a component and all its
children will rerender, given that (a) the state that has been set is different
from the current state and (b) none of the children is memoized. Let's focus on
(a) for now; checking if the state is different. React does this with a strict
equality check (===
). This check is very cheap, because is compares the
reference identity of the object.
Note
An alternative would be comparing the value of every property of the object to the same property on a previous object. Looping over these object properties would require a considerable amount of time if we are dealing with complex (nested) state.
Because we are using immutable updates, we can guarantee that:
-
If the contents of the state after the update are different from the contents
of the state before the update, the reference identity will be different.
-
If some part of the state remain the same, these parts will keep the same
identity.
Please take a look at the comments in the code for each example.
How to update a simple object with multiple keys. Each key has a primitive value
(a string in this case). Here we only read and update; we don't delete or create
state.
import { useState } from "react";
export default function App() {
const [todo, setTodo] = useState({
message: "",
dueDate: "",
});
function changeMessage(newMessage) {
setTodo((currentTodo) => {
return {
...currentTodo,
message: newMessage,
};
});
}
function changeDueDate(newDueDate) {
setTodo((currentTodo) => {
return {
...currentTodo,
dueDate: newDueDate,
};
});
}
return (
<div>
<input
type="text"
value={todo.message}
onChange={(e) => changeMessage(e.target.value)}
/>
<input
type="date"
value={todo.dueDate}
onChange={(e) => changeDueDate(e.target.value)}
/>
<pre>state: {JSON.stringify(todo, null, 2)}</pre>
</div>
);
}
A slightly more complex example. Here the state is an object which contains
several other objects.
import { useState } from "react";
export default function App() {
const [todo, setTodo] = useState({
message: {
title: "",
body: "",
},
author: {
firstName: "",
lastName: "",
},
dueDate: "",
});
function changeMessageBody(newBody) {
setTodo((currentTodo) => {
return {
...currentTodo,
message: {
...currentTodo.message,
body: newBody,
},
};
});
}
function changeMessageTitle(newTitle) {
setTodo((currentTodo) => {
return {
...currentTodo,
message: {
...currentTodo.message,
title: newTitle,
},
};
});
}
function changeAuthorFirstName(newFirstName) {
setTodo((currentTodo) => {
return {
...currentTodo,
author: {
...currentTodo.author,
firstName: newFirstName,
},
};
});
}
function changeAuthorLastName(newLastName) {
setTodo((currentTodo) => {
return {
...currentTodo,
author: {
...currentTodo.author,
lastName: newLastName,
},
};
});
}
function changeDueDate(newDueDate) {
setTodo((currentTodo) => {
return {
...currentTodo,
dueDate: newDueDate,
};
});
}
return (
<div>
<input
type="text"
placeholder="message body"
value={todo.message.body}
onChange={(e) => changeMessageBody(e.target.value)}
/>
<input
type="text"
placeholder="message title"
value={todo.message.title}
onChange={(e) => changeMessageTitle(e.target.value)}
/>
<input
type="text"
placeholder="first name"
value={todo.author.firstName}
onChange={(e) => changeAuthorFirstName(e.target.value)}
/>
<input
type="text"
placeholder="last name"
value={todo.author.lastName}
onChange={(e) => changeAuthorLastName(e.target.value)}
/>
<input
type="date"
value={todo.dueDate}
onChange={(e) => changeDueDate(e.target.value)}
/>
<pre>state: {JSON.stringify(todo, null, 2)}</pre>
</div>
);
}
An example with an array
import { useState } from "react";
export default function App() {
const [messages, setMessages] = useState([]);
function changeMessage(editedMessageIndex, editedMessage) {
setMessages((currentMessages) => {
return currentMessages.map((message, messageIndex) => {
if (messageIndex === editedMessageIndex) {
return editedMessage;
}
return message;
});
});
}
function deleteMessage(indexToDelete) {
setMessages((currentMessages) => {
return currentMessages.filter((message, messageIndex) => {
if (messageIndex === indexToDelete) {
return false;
}
return true;
});
});
}
function addMessage() {
setMessages((currentMessages) => {
return [...currentMessages, ""];
});
}
return (
<div>
{messages.map((message, messageIndex) => {
return (
<div>
<input
type="text"
placeholder="message"
value={message}
onChange={(e) => changeMessage(messageIndex, e.target.value)}
/>
<button
type="button"
onClick={() => {
deleteMessage(messageIndex);
}}
>
Delete
</button>
</div>
);
})}
<button
type="button"
onClick={() => {
addMessage();
}}
>
Add
</button>
<pre>state: {JSON.stringify(messages, null, 2)}</pre>
</div>
);
}
import { useState } from "react";
export default function App() {
const [todos, setTodos] = useState([]);
function changeTodoMessage(editedTodoIndex, editedMessage) {
setTodos((currentTodos) => {
return currentTodos.map((todo, index) => {
if (index === editedTodoIndex) {
return {
...todo,
message: editedMessage,
};
}
return todo;
});
});
}
function changeTodoDueDate(editedTodoIndex, editedDueDate) {
setTodos((currentTodos) => {
return currentTodos.map((todo, index) => {
if (index === editedTodoIndex) {
return {
...todo,
dueDate: editedDueDate,
};
}
return todo;
});
});
}
function deleteTodo(indexToDelete) {
setTodos((currentTodos) => {
return currentTodos.filter((todo, index) => {
if (index === indexToDelete) {
return false;
}
return true;
});
});
}
function addTodo() {
setTodos((currentTodos) => {
return [
...currentTodos,
{
message: "",
dueDate: "",
},
];
});
}
return (
<div>
{todos.map((todo, index) => {
return (
<div>
<input
type="text"
placeholder="message"
value={todo.message}
onChange={(e) => changeTodoMessage(index, e.target.value)}
/>
<input
type="date"
value={todo.dueDate}
onChange={(e) => changeTodoDueDate(index, e.target.value)}
/>
<button
type="button"
onClick={() => {
deleteTodo(index);
}}
>
Delete
</button>
</div>
);
})}
<button
type="button"
onClick={() => {
addTodo();
}}
>
Add
</button>
<pre>state: {JSON.stringify(todos, null, 2)}</pre>
</div>
);
}