Are you defining variables the right way?
Hidden bugs when React rerenders
A quick guide on when NOT to use React States, React Refs, and Local Variables.
TLDR (Code Examples and Demo Post TLDR)
- use React States when you want to Render it on the Screen.
- React States are NOT REDEFINED back to the original state values when a component rerenders.
- Define Variables INSIDE Components when it is some sort of derived state and it NEEDS to be REDEFINED each time a Component rerenders.
- Define Variables OUTSIDE Components if it should be Defined only ONCE. (like MAX_VALUE)
- use React refs when you want some value to be stored but NOT Render it on the screen.
- React Refs like React states are NOT REDEFINED back to the original state values when a component rerenders.
There are lots of ways we can define a variable in a React Component.
It depends on whether we need a state, a ref, or a plain variable.
However, when not implemented properly, it can cause several issues.
Let’s see how a state, a ref, and a plain variable behave when a Component is rerendered
Let’s consider an example where we have a user list state and an Add User Button. We will populate each user into the user list whenever we click on the button.
Initial Code
import { useState } from "react";
const App = () => {
const [users, setUsers] = useState([]);
const onAddUser = () => {
// It will populate users with id: 1, name: 'A'
// then id: 2, name: 'B' and so on
setUsers((prev) => {
const lastNameCodePoint = prev.at(-1)?.name?.charCodeAt(0);
return [
...prev,
{
id: prev.length + 1,
name: String.fromCharCode((lastNameCodePoint || 64) + 1)
}
];
});
};
return (
<div>
{users.map((user) => (
<div key={user.id}>Name - {user.name}</div>
))}
<button onClick={onAddUser}>Add User</button>
</div>
);
};
export default App;
When we do setUsers, users state is updated to the latest, Component Rerenders, and we see updated details on the screen.
Now let’s say we want to track the number of times we have clicked on the button.
Let’s try this by defining a local variable and incrementing it each time we click on the button.
Updated Code
import { useState } from "react";
const App = () => {
const [users, setUsers] = useState([]);
// NEW CODE LINE
// Define a local variable to track no of times onAddUser is clicked
let timesOnAddUserClicked = 0;
const onAddUser = () => {
// NEW CODE LINE
// Increment the variable each time onAddUser is clicked
timesOnAddUserClicked++;
setUsers((prev) => {
const lastNameCodePoint = prev.at(-1)?.name?.charCodeAt(0);
return [
...prev,
{
id: prev.length + 1,
name: String.fromCharCode((lastNameCodePoint || 64) + 1)
}
];
});
};
// NEW CODE LINE
// Log the no of times user is clicked
console.log("No of times Clicked", timesOnAddUserClicked);
return (
<div>
{users.map((user) => (
<div key={user.id}>Name - {user.name}</div>
))}
<button onClick={onAddUser}>Add User</button>
</div>
);
};
export default App;
Will the logs log out expected values or is there some hidden bug! Let’s see
Each time we click on Add User we expect that timesOnAddUserClicked would be incremented and we would get the correct log, but here we are getting 0 each time!
This is because…
A Local Variable defined inside React Component is Redefined each time the Component Rerenders.
So timesOnAddUserClicked is redefined to 0 each time we do setUsers.
So what’s the correct way? Use React Refs.
Let’s rewrite the code using refs now!
Updated Code using Refs
import { useRef, useState } from "react";
const App = () => {
const [users, setUsers] = useState([]);
// UPDATED CODE TO REFS
const timesOnAddUserClicked = useRef(0);
const onAddUser = () => {
// UPDATED CODE TO REFS
timesOnAddUserClicked.current++;
setUsers((prev) => {
const lastNameCodePoint = prev.at(-1)?.name?.charCodeAt(0);
return [
...prev,
{
id: prev.length + 1,
name: String.fromCharCode((lastNameCodePoint || 64) + 1)
}
];
});
};
// UPDATED CODE TO REFS
console.log("No of times Clicked", timesOnAddUserClicked.current);
return (
<div>
{users.map((user) => (
<div key={user.id}>Name - {user.name}</div>
))}
<button onClick={onAddUser}>Add User</button>
</div>
);
};
export default App;
Let’s see if we get expected values from the logs or not.
This works because…
A React Ref is NOT Redefined each time the Component Rerenders. The value is persisted across renders!
So you might have a question now is there a use case for which we can use local vanilla variables, yes there is!
Let’s say we have a use case to show no of users added and we want to update each time a user is added.
In this case, we have two options. First Option is to Add one more state and set it each time a user is added. This way when a component is rerendered we will see the updated value on the screen.
But a better option is the Second Option.
As no of users is directly dependent on the users state, we don’t need to track another state to keep the count.
What we can do is define a local variable, and when a component is rerendered we will be able to see the latest count of users added.
Let’s see the code!
Updated Code to show no of users added
import { useRef, useState } from "react";
const App = () => {
const [users, setUsers] = useState([]);
const timesOnAddUserClicked = useRef(0);
const onAddUser = () => {
timesOnAddUserClicked.current++;
setUsers((prev) => {
const lastNameCodePoint = prev.at(-1)?.name?.charCodeAt(0);
return [
...prev,
{
id: prev.length + 1,
name: String.fromCharCode((lastNameCodePoint || 64) + 1)
}
];
});
};
console.log("No of times Clicked", timesOnAddUserClicked.current);
// NEW LINE (Local Variable - gets redefined on each rerender)
const noOfUsersAdded = users.length;
return (
<div>
{users.map((user) => (
<div key={user.id}>Name - {user.name}</div>
))}
<button onClick={onAddUser}>Add User</button>
{/* NEW LINE (To display no of users) */}
<div>No of Users: {noOfUsersAdded}</div>
</div>
);
};
export default App;
What other cases you can think of? Do let me know!
Thanks!