React State and Arrays
Learn how to use React state to work with arrays and what a JavaScript mutable and immutable are.
Table of Contents 📖
JavaScript Mutable
To understand why arrays are treated differently by React, we need to first understand mutable vs immutable in JavaScript. A mutable is a type of variable that can be changed without creating a new value. This is because they are stored in a variable by reference. In JavaScript, objects, arrays, and functions are mutable. To demonstrate, lets create two variables that reference the same array.
const myArr1 = [1, 2, 3];
const myArr2 = myArr1;
myArr1[0] = 27;
console.log(myArr2[0]) // 27
Here, when we alter the first element in myArr1 the same happens to myArr2. This is because both of these variables are references to the same array.
JavaScript Immutable
An immutable is the opposite of a mutable, it is a value that cannot be changed once it is created. In JavaScript, booleans, numbers, and strings are immutable.
const myVal1 = 1;
const myVal2 = myVal1;
myVal1 = 27;
console.log(myVal2); // 1
Here, myVal1 and myVal2 are not references but contain the actual variable value. Therefore, altering the copy does not alter the original. These differences between mutables and immutables need to be accounted for in React.
React State and Mutables
In React, both mutables and immutables should be treated like immutables. As an array is a mutable, we need to treat it like an immutable. This means that when updating an array stored in state we need to use non-mutating methods. In other words, we need to create a new array or make a copy of an existing one. We should not push new values into it, call pop to remove a value, reassign the value at a specific index, etc. This is because if we store a mutable in React state, it will actually store its reference. Pushing and popping values from the array does not update the reference but rather what it points to. For a demonstration, lets create a simple React app that manages an array in state.
import { useState } from "react";
const App = () => {
console.log('Rendering App!');
const [people, setPeople] = useState([]);
const addPeople = () => {
people.push('WittCode');
}
return (
<button onClick={addPeople}>Add a Person</button>
)
}
export default App;
Here, whenever the App component is being rendered Rendering App! will be logged to the console. When we click the button we add the string WittCode to the people array.
However, notice how the App component is not being re-rendered when the button is clicked and a new element is added to the people array. This is because the people array is being mutated but the reference contained in state is not changing. No state change, no re-render. If we want the component to re-render we need to provide a new array. We can do this with the ES6 spread syntax.
import { useState } from "react";
const App = () => {
console.log('Rendering App!');
const [people, setPeople] = useState([]);
const addPeople = () => {
setPeople([...people, 'WittCode']);
}
return (
<button onClick={addPeople}>Add a Person</button>
)
}
export default App;
Now when we click the button the component is re-rendered.
This is because the ES6 spread syntax allows us to create a copy of an array into another array. In other words, the reference being held by the App component's state is changed as a new array is being used entirely. Besides the spread operator, we can use any method that returns a new array such as map.
import { useState } from "react";
const App = () => {
console.log('Rendering App!');
const [people, setPeople] = useState([]);
const addPeople = () => {
const newPeople = people.map(p => p);
setPeople(newPeople);
}
return (
<button onClick={addPeople}>Add a Person</button>
)
}
export default App;
We can actually use any method as long as it returns a new array and isn't mutational. Other options being filter, concat, slice, etc.