Improper state management in ReactJS
Nikolay
Posted on May 23, 2023
Let's consider a simple example to illustrate the losing state problem in a React application when storing a generic class instance in a component's state.
Create a generic class called Counter
:
class Counter {
constructor() {
// Setting shared state of the class
this.count = 0;
}
increment() {
// Changing shared state of the class
this.count += 1;
}
getCount() {
return this.count;
}
}
Now, let's create a React component that uses this Counter
class:
import React, { useState } from 'react';
function CounterComponent() {
// Initializing instance of the Counter class
//and save it to **ReactJS state**
const [counterInstance, setCounterInstance] = useState(new Counter());
const handleIncrement = () => {
// Let's increment the counter and save it to the react state
counterInstance.increment();
// What can go wrong, huh?
setCounterInstance(counterInstance);
};
return (
<div>
<h1>Count: {counterInstance.getCount()}</h1>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
export default CounterComponent;
In this example, we have a CounterComponent
that initializes and stores an instance of the Counter
class in its state. When the user clicks the "Increment" button, the handleIncrement
function is called, which increments the count and updates the state with the modified counterInstance
.
The problem with this implementation is that React's state updates are asynchronous, and it uses a shallow merge to update the state. As a result, the Counter
class instance is not handled correctly, and you may experience issues with the component's behavior or state updates.
To fix this problem, let's refactor the code to store the count value as a plain JavaScript object and keep the logic related to the Counter
class separate:
import React, { useState, useEffect, useRef } from "react";
// Pretty much the same class but with one important change
class Counter {
constructor(initialCount) {
// Instance will be initialized only once
console.log("init");
this.count = initialCount;
}
increment() {
// The method returns value to be saved in react state,
// not in the shared state of the instance
this.count += 1;
return this.count;
}
}
function CounterComponent() {
const [counterInstance, setCounterInstance] = useState();
const [count, setCount] = useState(0);
useEffect(() => {
// Save the instance of the Counter class to the react state
// It is better to use proper state management library here
setCounterInstance(new Counter(0));
}, []);
const handleIncrement = () => {
const newCount = counterInstance.increment();
setCount(newCount);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
export default CounterComponent;
Now we use useEffect
to create the Counter
class instance only once when the component mounts. Then, you store the Counter
class instance in the React state with setCounterInstance
.
Conclusion:
To avoid issues with asynchronous state updates and shallow merging in React, it's better to store simple data types in the state, rather than class instances, and manage complex logic separately.
However, it's important to note that storing class instances in React state is generally not a best practice. It's more common to keep state as simple as possible, typically using JavaScript primitives and plain objects.
Posted on May 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024