5

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.

12
  • What are you trying to do in simple words? Commented May 11, 2022 at 19:21
  • I have a class. I need that component that used its content rerender on its change. I want to do it in a elegant and canonical way. Commented May 11, 2022 at 19:30
  • You want to persist the class instance across re-renders? Commented May 11, 2022 at 19:32
  • @AseemGautam, it's pretty easy (useRef). I want to rerender component on its properties change also in a elegant way. Commented May 11, 2022 at 20:31
  • @nitrovatter How do you know if the class instance "changes"? You'll need to give React something to work with. Commented May 11, 2022 at 22:35

2 Answers 2

3

Create an abstract class that tracks "clients" that subscribe/listen to it.

class EventEmitter {
  constructor() {
    this.listeners = [];
  }

  addListener(listener) {
    this.listeners.push(listener);
  }

  removeListener(listenerToRemove) {
    this.listeners = this.listeners.filter(listener => listenerToRemove !== listener);
  }

  notify() {
    this.listeners.forEach(listener => listener());
  }

  getListeners() {
    return this.listeners;
  }
}

Extend your class with EventEmitter to allow React to listen to it for changes

class ReallyHugeClass extends EventEmitter { // <-- Extend the class
  constructor() {
     super();
     this.counter = 0;
  }

  increment = () => {
     this.counter += 1
     this.notify() // <--- Fire notify() after you update state  
  }
}

Create a React hook that can take in a class instance that extends EventEmitter and add listeners on mount (and remove on unmount)

import { useEffect, useState, useRef } from 'react';

const useEventEmitter = (instance) => {
  const [, forceUpdate] = useState(0); // <-- Simply incrementing a counter will cause a re-render
  const listenerRef = useRef(null); // <-- Hold a reference to the current listener function across renders

  useEffect(() => {
    const newListener = () => {
      forceUpdate(prev => prev + 1);
    };
    
    // Remove any existing listeners
    if (listenerRef.current) {
      instance.removeListener(listenerRef.current);
    }

    // Add the listener
    listenerRef.current = newListener;
    instance.addListener(newListener);

    // Cleanup listener and remove on unmount
    return () => {
      instance.removeListener(newListener);
    };
  }, [instance]);

  return instance;
};


You can define the class outside the render (same instance will be used across renders or you can useRef inside the component) and use the hook to listen for changes in your class and your React component will re-render everytime notify() is called from the class.

const instance = new ReallyHugeClass()  

const Component = () => {
     useEventEmitter(instance); // <-- "connect" to the class and make this component listen for changes to re-render
 
    return (
        <button onClick={instance.increment}> 
            {instance.counter}
        </button>
     )
}
Sign up to request clarification or add additional context in comments.

Comments

0

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>

8 Comments

I know this solution. I am wondering if there any other beautiful way to do it. For example, in Vue or MobX it's pretty simple: I can make the class properties reactive and that's all.
I've fixed the misspell.
What is the point of the effect that does nothing?
@nitrovatter If you're looking for MobX, why not just use it? React does not provide any of this out of the box, if that is what your question was.
@Bergi, I can't use MobX because it's a big dependency and I can't install it only for rare use-cases.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.