question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Observer is constantly destroyed and replaced when using onResize callback

See original GitHub issue

Hello @ZeeCoder! I’ve recently started on adding some ResizeObservers into my React app. I found this hook, and it’s great, and I love it. Most of the instances of this hook I have added work great, but the last instance of this hook I needed to add before I was done has broken my entire app. What exactly is causing this has been very elusive, and I’ve spent a few hours trying to track it down. I was able to determine that the bug was not in my code, nor does it have anything to do with the polyfill, and that it is indeed here. After going line-by-line through your source code and adding lots of console.log calls to it, I found it.

The bug is that when using the onResize callback, the hook destroys its existing observer and creates a new one every single time the hook is called, that is, every single time the component is rendered. In the particular case I refer to, this bug breaks my app, and I’ll explain how that happens. However, this bug occurs in all cases; it’s just that you normally wouldn’t notice it.

The reason why this happens is because you have an effect hook that creates and destroys the observer, and that hook has the onResize callback as a dependency. Now, obviously, if the hook’s dependencies aren’t equal, the hook runs again. Consider the example you wrote in the readme:

import React from "react";
import useResizeObserver from "use-resize-observer";

const App = () => {
  // width / height will not be returned here when the onResize callback is present
  const { ref } = useResizeObserver({
    onResize: ({ width, height }) => {
      // do something here.
    }
  });

  return <div ref={ref} />;
};

Conceptually, the onResize callback is completely static between renders of the component. But here’s the kicker: two functions are equal if and only if they are the same object i.e. they have the same memory reference. It actually uses Object.is() to compare dependencies, but this is the case for strict equality too. The function may seem static, but you’ll notice that it’s defined within the component. As such, a new function is created every single time App renders. This means that in this example and all examples like it, the onResize callback ‘changes’ on literally every render. It is impossible to pass the same function twice. This causes the observer to be destroyed and re-created on every render.

Now, what you could do is something like this.

import React from "react";
import useResizeObserver from "use-resize-observer";

const onResize = ({ width, height }) => {
  // do something here.
}

const App = () => {
  // width / height will not be returned here when the onResize callback is present
  const { ref } = useResizeObserver({ onResize });
  return <div ref={ref} />;
};

And guess what! The callback is is declared statically, and not just conceptually but literally testing it in my app, doing this completely eliminates the problem.

The case of this bug that is causing so much strife for me is a case where I have infinite render loop, causing everything to hang and crash. Inside the callback, I’m setting the dimensions as state, which causes the component to re-render with that new state. The fact that it re-renders even if the values are the same is an implementation detail of what I’m doing, but because of that, and because ResizeObservers fire once immediately when set to observe an element, this creates a constant loop of re-rendering and creating and destroying the observer.

I note from this test that the ability to change the callback and for it to be called immediately is a feature you deliberately sought to support. However, I’m not sure this actually makes sense. Partially, there is just no good way to do this, because there is no good way to detect an actual change in function in this context, as discussed. But I’m also not sure it makes conceptual sense. I think if I changed the callback of an instance of the hook that already exists, for… some reason (I’m not sure what the use case for this actually is), I would just expect it to get called next time the observer fires; I don’t see any intuitive reason for it to be called immediately. The fact that you’re destroying and re-creating the whole ResizeObserver when the callback changes I don’t think is at all intuitive. So if I were you, I would just remove the callback as a dependency of the effect hook. I think you could use a ref to store the callback as it changes to make it available to the ResizeObserver already in existence.

An additional minor thing I caught which you could perhaps call a bug is that it seems that you intend for the hook to not update anything if the values doesn’t actually change. This just doesn’t work with the onResize callback. It checks the current values with the previous values, but the previous values are never set in the onResize branch, so that condition is trivially true when using a onResize callback. In theory, if this worked, I would never have had this problem. However, I feel like this check should actually just be done away with. Perhaps you can think of same cases that I can’t, but in theory, there should never be any case where a ResizeObserver fires for an element, but that element has not changed size from the last time it fired. I feel like if that ever happens, it’s inherently a bug. All this check does is make said bug undetectable.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:10 (6 by maintainers)

github_iconTop GitHub Comments

4reactions
ZeeCodercommented, Mar 27, 2020

@jtomaszewski that’s a very good point. I’ll start tackling these issues on the repo soon I think as I’ve been laid off (along with the 2/3 of the company) due to the effects the coronavirus pandemic had on the company.

1reaction
ZeeCodercommented, Jun 1, 2020

No, it’s currently one resize observer instance for each hook instance. (previously depending on circumstances even within the hook a new resize observer instance would be created)

Those are about using a single shared resize observer for all hooks.

On Mon, 1 Jun 2020, 21:48 Taylor Jones, notifications@github.com wrote:

That way the RO never need to be recreated, and changes to the onResize callback are still respected as it’s stored and updated in a mutable ref.

@ZeeCoder https://github.com/ZeeCoder If I’m interpreting the above and the associated PR correctly, this means #24 https://github.com/ZeeCoder/use-resize-observer/issues/24/#26 https://github.com/ZeeCoder/use-resize-observer/pull/26 has been fixed here as well?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ZeeCoder/use-resize-observer/issues/32#issuecomment-637066560, or unsubscribe https://github.com/notifications/unsubscribe-auth/AA4CKEV2Z3EZ5UV6QL5VEW3RUQAZVANCNFSM4LEWGPZA .

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to use ResizeObserver with Angular
ResizeObserver is a new API which allows us to react to element resizing. There are a few steps required to use it properly...
Read more >
The ResizeObserver API: A tutorial with examples
Using a ResizeObserver , we can call a function whenever an element is resized, much like listening to a window resize event.
Read more >
javascript - How to detect DIV's dimension changed?
A newer standard for this is the Resize Observer api, with good browser support. ... var onResize = function(element, callback) { if (!onResize....
Read more >
How to use ResizeObserver with Angular - Chris Kohler
ResizeObserver is a new API which allows us to react to element resizing. There are a few steps required to use it properly...
Read more >
ResizeObserver#observe() firing the callback immediately ...
Let's prevent the loop by checking the image that is going to update with a new and check for width/height of browser's default...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found