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.
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} />;
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With