DEV Community

Nikolay
Nikolay

Posted on

Improper state management in ReactJS

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)