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>;
}
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.