'How to make a class value reactive in React

I have a simple class, something like this:

class ReallyHugeClass {
  constructor() {
     this.counter = 0;
  }
  increment = () => {
     this.counter += 1
  }
}

If I use it in the code in a straightforward way it won't keep its state. The class will be recreated every time on render and it's not reactive at all.

const Component = () => {
   const instance = new ReallyHugeClass();
   return (
       <button onClick={instance.increment}> 
          {instance.counter}
       </button>
   )
}

Don't rush to say: you don't need the class! Write this:

const Component = () => {
   const [counter, setCounter] = useState(0);
   return (
       <button onClick={() => { setCounter(value => value + 1) }}> 
          {counter}
       </button>
   )
}

I used the ridiculously small class example, but the real one is complicated. Very complicated. I can't just split it into the set of useState calls.

Let's go forward. I can wrap the instance to useRef to save its value.

const Component = () => {
   const instance = useRef(new ReallyHugeClass());
   return (
       <button onClick={instance.current.increment}> 
          {instance.current.counter}
       </button>
   )
}

The value is saved, but it's still not reactive. I can somehow force the component to rerender by passing the corresponding callback to class, but it looks awkwardly.

What's the right pattern to solve such task in React? It looks that it's quite likely situation.



Solution 1:[1]

One solution would be to use useRef and force rendering with a useState. Here an example:

const { useRef, useState } = React;

      class ReallyHugeClass {
        constructor() {
          this.counter = 0;
        }
        increment() {
          this.counter += 1;
          console.log(this.counter);
        }
      }

      function App() {
        const instance = useRef(new ReallyHugeClass());
        const [forceRender, setForceRender] = useState(true);

        return (
          <button
            onClick={() => {
              instance.current.increment();
              setForceRender(!forceRender);
            }}
          >
            {instance.current.counter}
          </button>
        );
      }

      const root = ReactDOM.createRoot(document.getElementById("root"));
      root.render(
        <>
          <App />
          <App />
        </>
      );
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.production.min.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
    ></script>
    <div id="root"></div>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1