Is it possible that React modifies a reference?

Viewed 73

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.

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.

Related