Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't recreate ResizeObserver on callback reference change #158

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/hooks/useResizeObserver/useResizeObserver.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import { Meta } from '@storybook/blocks';

# useResizeObserver

This hook allows you to add a ResizeObserver for an element and remove it when the component
This hook allows you to add a `ResizeObserver` to an element and remove it when the component
unmounts.

## Reference

```ts
function useResizeObserver(ref: RefObject<Element>, callback: ResizeObserverCallback): void;
function useResizeObserver(
target: Unreffable<Element | null>,
callback: ResizeObserverCallback,
): void;
```

## Usage

Using the `useResizeObserver` hook using a RefObject:

```tsx
function DemoComponent() {
const ref = useRef<HTMLDivElement>(null);
Expand All @@ -26,3 +31,18 @@ function DemoComponent() {
return <div ref={ref}></div>;
}
```

Using the `useResizeObserver` hook using state:

```tsx
function DemoComponent() {
const [element, setElement] = useState<HTMLDivElement | null>(null);

useResizeObserver(element, () => {
// eslint-disable-next-line no-console
console.log('Element resized');
});

return <div ref={setElement}></div>;
}
```
32 changes: 21 additions & 11 deletions src/hooks/useResizeObserver/useResizeObserver.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { StoryObj } from '@storybook/react';
import { type ReactElement, useRef } from 'react';
import { useRef, useState } from 'react';
import { useResizeObserver } from './useResizeObserver.js';

export default {
title: 'hooks/useResizeObserver',
};

function DemoComponent(): ReactElement {
const ref = useRef<HTMLDivElement>(null);
export const UsingRefObject: StoryObj = {
render() {
const ref = useRef<HTMLDivElement>(null);

useResizeObserver(ref, () => {
// eslint-disable-next-line no-console
console.log('Element resized');
});
useResizeObserver(ref, () => {
// eslint-disable-next-line no-console
console.log('Element resized');
});

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

export const Demo: StoryObj = {
export const UsingState: StoryObj = {
render() {
return <DemoComponent />;
const [element, setElement] = useState<HTMLDivElement | null>(null);

useResizeObserver(element, () => {
// eslint-disable-next-line no-console
console.log('Element resized');
});

return <div ref={setElement}></div>;
},
};
29 changes: 21 additions & 8 deletions src/hooks/useResizeObserver/useResizeObserver.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { type RefObject, useEffect } from 'react';
import { useEffect } from 'react';
import { unref, type Unreffable } from '../../index.js';
import { useRefValue } from '../useRefValue/useRefValue.js';

/**
* This hook allows you to add a ResizeObserver for an element and remove it
* This hook allows you to add a ResizeObserver to an element and remove it
* when the component unmounts.
*
* @param ref - The ref to observe
* @param callback - The callback to fire when the element resizes
*/
export function useResizeObserver(ref: RefObject<Element>, callback: ResizeObserverCallback): void {
export function useResizeObserver(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a separate ticket to support unobserve, e.g. having a boolean arg that defaults to true, but can be set to false to unobserve ?

Copy link
Collaborator Author

@leroykorterink leroykorterink May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function useResizeObserver(
  target: Unreffable<Element>,
  callback: ResizeObserverCallback,
  options?: ResizeObserverOptions,
  enabled = true,
): void

Implementation will be similar to this:

export function useResizeObserver(
  target: Unreffable<Element>,
  callback: ResizeObserverCallback,
  options?: ResizeObserverOptions,
  enabled = true,
): void {
  const memoizedOptions = useMemo(
    () => options,
    [JSON.stringify(options)],
  );

  const callbackRef = useRefValue(callback);
  const resizeObserver = useClientSideValue(
    () =>
      new ResizeObserver((entries, observer) => {
        callbackRef.current?.(entries, observer);
      }),
  );

  useEffect(() => {
    const element = unref(target);

    if (element === null) {
      return;
    }

    if (enabled) {
      resizeObserver?.observe(element, memoizedOptions);
    } else {
      resizeObserver?.unobserve(element);
    }

    return () => {
      resizeObserver?.unobserve(element);
    };
  }, [enabled, memoizedOptions, resizeObserver, target]);
}

target: Unreffable<Element | null>,
callback: ResizeObserverCallback,
): void {
const callbackRef = useRefValue(callback);

useEffect(() => {
const resizeObserverInstance = new ResizeObserver(callback);
const element = unref(target);

if (ref.current === null) {
throw new Error('`ref.current` is undefined');
if (element === null) {
return;
}

resizeObserverInstance.observe(ref.current);
const resizeObserverInstance = new ResizeObserver(
(entries: Array<ResizeObserverEntry>, observer: ResizeObserver) => {
callbackRef.current?.(entries, observer);
},
);

resizeObserverInstance.observe(element);
leroykorterink marked this conversation as resolved.
Show resolved Hide resolved

return () => {
resizeObserverInstance.disconnect();
};
}, [ref, callback]);
}, [callbackRef, target]);
}