skip to Main Content

I am trying to detect a variable change in react. The variable is a class property. That is set when an asynchronous event takes place.

You can see below the example of my class.

Inside method listenForIncomingMessage(), something asynchronous happens and it sets the data property of the class.

class Message() {
  static #instance: Message;
  data!: any;

  static getInstance(): Message {
    if (!Message.#instance) {
      Message.#instance = new Message();
    }

    return Message.#instance;
  }
  
  listenForIncomingMessage() {

    // Something asynchronous is happening here
    setTimeout(() => {
      this.data = 'hello world';
    }, 2000);
  }
}

Then this class is initialized in main.tsx like this and it is passed through a prop like this

const MainPage = () => {

  const msg = Message.getInstance();
  msg.listenForIncomingMessage();

  return (
    <div className="panel">
      <Result message={ msg } />
    </div>
  );
};

ReactDOM.createRoot(
  document.getElementById("root") as HTMLDivElement,
).render(<MainPage />);

Finally inside Result.tsx, I need to detect the latest value for msg.data.

export default function Result({ msg }) {
  
  let responseObj = useRef(msg.data);

  useEffect(() => {             <------ This is not triggered
    console.log(msg.data); 
  }, [msg.data]);

)};

I could get the value from a callback, or event listener inside Result.tsx but I am trying to see if there is a react hook solution.

Thank you.

2

Answers


  1. The issue arises in your scenario because React won’t directly detect changes in plain class properties like msg.data, since these changes don’t trigger React’s state update. To solve this, you can use useState and useEffect to manage and monitor the changes in the data property.
    To detect changes in a class property (msg.data) using React hooks, you can:

    1. Add a method in the class (onDataChanged) to notify when data changes.
    2. Use useState in the component to hold the value of data.
    3. Use useEffect to subscribe to the change and update the state when data changes.

    Here’s a the solution:

    class Message {
      static #instance: Message;
      data!: any;
      private dataChangeCallback?: () => void;
    
      static getInstance() {
        if (!Message.#instance) {
          Message.#instance = new Message();
        }
        return Message.#instance;
      }
    
      onDataChanged(callback: () => void) {
        this.dataChangeCallback = callback;
      }
    
      listenForIncomingMessage() {
        setTimeout(() => {
          this.data = 'hello world';
          this.dataChangeCallback?.(); // Notify change
        }, 2000);
      }
    }
    
    const Result = ({ message }: { message: Message }) => {
      const [response, setResponse] = useState(message.data);
    
      useEffect(() => {
        message.onDataChanged(() => setResponse(message.data));
      }, [message]);
    
      return <p>Response: {response}</p>;
    };
    

    This makes msg.data reactive in the component.

    Login or Signup to reply.
  2. Changes to an object property are not tracked by React, and don’t cause it re-render the parent and children.

    You can wrap the instance in Proxy with a set() handler to catch changes to the data property, and set a state accordingly.

    In addition, since listening to external events is a side effect, this should happen inside a useEffect block. This will prevent an infinite loop of trigger listen -> set state -> re-render -> trigger listen -> …

    const MainPage = () => {
      const [message, setMessage] = useState('');
    
      useEffect(() => {
        const msg = new Proxy(Message.getInstance(), {
          set(target, key, value) {
              target[key] = value;
              
              // update the component's state only if the key is data
              if(key === 'data') setMessage(value);
              
              return true;
          }
        });
    
        msg.listenForIncomingMessage();
      }, []);
    
      return (
        <div className="panel">
          <Result message={message} />
        </div>
      );
    };
    
     const Result = ({ message }) => {
      useEffect(() => {
        console.log(message); 
      }, [message]);
    
    )};
    

    If you need to use the proxied instance for other things, you can wrap it in useMemo. The listen to changes, however, still needs to happen inside useEffect:

    const MainPage = () => {
      const [message, setMessage] = useState('');
      
      const msg = useMemo(() => new Proxy(Message.getInstance(), {
        set(target, key, value) {
            target[key] = value;
    
            // update the component's state only if the key is data
            if(key === 'data') setMessage(value);
    
            return true;
        }
      }));
    
      useEffect(() => {
        msg.listenForIncomingMessage();
      }, [msg]);
    
      return (
        <div className="panel">
          <Result message={message} />
        </div>
      );
    };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search