Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible that React modifies a reference?

I made a small experiment with implementing an observer pattern manually in React (*). It basically works, but with a highly unexpected detail. Consider this minimal example:

class Observer {
  constructor() {
    this.callbacks = [];
  }

  register(callback) {
    console.log("received callback register");
    this.callbacks.push(callback);
    console.log(`number of callbacks: ${this.callbacks.length}`);
  }

  call() {
    console.log(`calling ${this.callbacks.length} callbacks`);
    for (let callback of this.callbacks) {
      callback();
    }
  }
}

function Main() {
  const observer = useRef(new Observer());

  useEffect(() => {
    observer.current.call();
  }, [observer]);

  return <SubComponent observer={observer.current} />;
}

function SubComponent({ observer }) {
  console.log("registering observer");
  observer.register(() => {
    console.log("callback called");
  });
  return <div>Hello World</div>;
}

CodeSandbox

In the console log this produces:

registering observer
received callback register
number of callbacks: 1
calling 2 callbacks
callback called
callback called

As you can see, the number of registered callbacks has suddenly changed to 2, even though only 1 callback has been registered. How is this possible? Do I have a blind spot or is this somehow an implication of how React works?


(*) I know that this problem can be solved by a combination of useImperativeHandle and forwardRef. The above is just an experiment to investigate alternatives, and I'm asking for learning purposes.

like image 279
bluenote10 Avatar asked Jan 27 '21 18:01

bluenote10


1 Answers

Because you put the register logic in render function (function component's body), it will register it on every component's render.

And since you have StrictMode wrapper, it invoked twice:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • ...
  • Function component bodies

You can either remove the StrictMode (not recommended) or write the logic in useEffect as I guess you want it registered on observer change:

function Main() {
  const observer = useRef(new Observer());

  useEffect(() => {
    observer.current.call();
  }, [observer]);

  return <SubComponent observer={observer.current} />;
}

Edit patient-cache-emkoo

Note that in StrictMode the logs are silenced, so you don't see the second console.log("registering observer");

Starting with React 17, React automatically modifies the console methods like console.log() to silence the logs in the second call to lifecycle functions.

like image 195
Dennis Vash Avatar answered Oct 02 '22 17:10

Dennis Vash