498

useResizeObserver

Hook that gives you resize observer state

sensorslowtest coverage
DT
Design Team23:05
Daria: pushed the new icons 🎨3
AC
Dmitry Babin12:03
See you at the meeting tomorrow
RU
reactuse11:48
You: just shipped useResizeObserver1
import type { PointerEvent } from 'react';

import { useResizeObserver } from '@siberiacancode/reactuse';
import { GripVerticalIcon } from 'lucide-react';
import { useRef, useState } from 'react';

import { cn } from '@/utils/lib';

interface Chat {
  id: string;
  initials: string;
  message: string;
  name: string;
  online?: boolean;
  time: string;
  unread?: number;
}

const CHATS: Chat[] = [
  {
    id: '1',
    name: 'Design Team',
    message: 'Daria: pushed the new icons 🎨',
    time: '23:05',
    initials: 'DT',
    unread: 3,
    online: true
  },
  {
    id: '2',
    name: 'Dmitry Babin',
    message: 'See you at the meeting tomorrow',
    time: '12:03',
    initials: 'AC',
    online: true
  },
  {
    id: '3',
    name: 'reactuse',
    message: 'You: just shipped useResizeObserver',
    time: '11:48',
    initials: 'RU',
    unread: 1
  }
];

const COLLAPSED_WIDTH = 66;
const EXPANDED_MIN = 240;
const EXPANDED_MAX = 420;
const SNAP_POINT = (COLLAPSED_WIDTH + EXPANDED_MIN) / 2;

const getEntryWidth = (entry: ResizeObserverEntry) =>
  entry.borderBoxSize[0]?.inlineSize ?? entry.contentRect.width;

const Demo = () => {
  const [width, setWidth] = useState(340);
  const [expanded, setExpanded] = useState(true);
  const draggingRef = useRef(false);
  const dragRef = useRef({ startWidth: 340, startX: 0 });

  const resizeObserver = useResizeObserver<HTMLDivElement>({
    box: 'border-box',
    onChange: ([entry]) => setExpanded(getEntryWidth(entry) >= EXPANDED_MIN)
  });

  const onPointerDown = (event: PointerEvent<HTMLButtonElement>) => {
    draggingRef.current = true;
    dragRef.current = {
      startWidth: width,
      startX: event.clientX
    };
    event.currentTarget.setPointerCapture(event.pointerId);
  };

  const onPointerMove = (event: PointerEvent<HTMLButtonElement>) => {
    if (!draggingRef.current) return;

    const rawWidth = Math.min(
      EXPANDED_MAX,
      Math.max(COLLAPSED_WIDTH, dragRef.current.startWidth + event.clientX - dragRef.current.startX)
    );

    setWidth(rawWidth < SNAP_POINT ? COLLAPSED_WIDTH : Math.max(EXPANDED_MIN, rawWidth));
  };

  const onPointerUp = (event: PointerEvent<HTMLButtonElement>) => {
    draggingRef.current = false;
    event.currentTarget.releasePointerCapture(event.pointerId);
  };

  return (
    <section className='flex flex-col items-center gap-3 p-4'>
      <div className='relative'>
        <div
          ref={resizeObserver.ref}
          className='bg-card no-scrollbar flex flex-col overflow-hidden rounded-2xl'
          style={{ width }}
        >
          {CHATS.map((chat) => (
            <div
              key={chat.id}
              className={cn(
                'hover:bg-muted/50 flex cursor-pointer items-center gap-3 py-2.5 transition-colors',
                expanded ? 'px-3' : 'justify-center px-0'
              )}
            >
              <div className='relative shrink-0'>
                <div data-size='lg' data-slot='avatar'>
                  <span data-slot='avatar-fallback'>{chat.initials}</span>
                </div>
                {chat.online && (
                  <span className='ring-card absolute right-0 bottom-0 size-3 rounded-full bg-green-500 ring-2' />
                )}
                {!expanded && !!chat.unread && (
                  <span className='bg-primary text-primary-foreground absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full text-[10px] font-semibold'>
                    {chat.unread}
                  </span>
                )}
              </div>

              {expanded && (
                <div className='flex min-w-0 flex-1 flex-col'>
                  <div className='flex items-center justify-between gap-2'>
                    <span className='text-foreground truncate text-sm font-medium'>
                      {chat.name}
                    </span>
                    <span className='text-muted-foreground shrink-0 text-xs'>{chat.time}</span>
                  </div>
                  <div className='flex items-center justify-between gap-2'>
                    <span className='text-muted-foreground truncate text-xs'>{chat.message}</span>
                    {!!chat.unread && (
                      <span className='bg-primary text-primary-foreground flex size-4 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold'>
                        {chat.unread}
                      </span>
                    )}
                  </div>
                </div>
              )}
            </div>
          ))}
        </div>

        <button
          aria-label='Resize'
          className='border-border bg-card text-muted-foreground hover:text-foreground absolute top-1/2 -right-3 z-10 flex size-6 -translate-y-1/2 cursor-ew-resize touch-none items-center justify-center rounded-full border shadow-sm select-none'
          data-variant='unstyled'
          type='button'
          onPointerDown={onPointerDown}
          onPointerMove={onPointerMove}
          onPointerUp={onPointerUp}
        >
          <GripVerticalIcon className='size-3.5' />
        </button>
      </div>
    </section>
  );
};

export default Demo;
This hook uses ResizeObserver browser api to provide enhanced functionality. Make sure to check for compatibility with different browsers when using this api

Installation

pnpm add @siberiacancode/reactuse

Usage

const { entries, observer } = useResizeObserver(ref);
// or
const { ref, entries, observer } = useResizeObserver();
// or
const { ref, entries, observer } = useResizeObserver((entries) => console.log(entries));
// or
const { entries, observer } = useResizeObserver(ref, (entries) => console.log(entries));

Type Declarations

import type { HookTarget } from '@/utils/helpers';

import type { StateRef } from '../useRefState/useRefState';

export type UseResizeObserverCallback = (
  entries: ResizeObserverEntry[],
  observer: ResizeObserver
) => void;

export interface UseResizeObserverOptions extends ResizeObserverOptions {
  /** The enabled state of the resize observer */
  enabled?: boolean;
  /** The callback to execute when resize is detected */
  onChange?: UseResizeObserverCallback;
}

export interface UseResizeObserverReturn {
  /** The resize observer entries */
  entries?: ResizeObserverEntry[];
  /** The resize observer instance */
  observer?: ResizeObserver;
}

export interface UseResizeObserver {
  <Target extends Element>(
    options?: UseResizeObserverOptions,
    target?: never
  ): UseResizeObserverReturn & { ref: StateRef<Target> };

  (target: HookTarget, options?: UseResizeObserverOptions): UseResizeObserverReturn;

  <Target extends Element>(
    callback: UseResizeObserverCallback,
    target?: never
  ): UseResizeObserverReturn & { ref: StateRef<Target> };

  (target: HookTarget, callback: UseResizeObserverCallback): UseResizeObserverReturn;
}

API

Parameters

NameTypeDefaultNote
targetHookTarget-The target element to observe
options.enabledbooleantrueThe enabled state of the resize observer
options.boxResizeObserverBoxOptions-The box model to observe
options.onChangeUseResizeObserverCallback-The callback to execute when resize is detected

Returns

UseResizeObserverReturn

Parameters

NameTypeDefaultNote
options.enabledbooleantrueThe enabled state of the resize observer
options.boxResizeObserverBoxOptions-The box model to observe
options.onChangeUseResizeObserverCallback-The callback to execute when resize is detected

Returns

UseResizeObserverReturn & { ref: StateRef<Target> }

Parameters

NameTypeDefaultNote
callbackUseResizeObserverCallback-The callback to execute when resize is detected

Returns

UseResizeObserverReturn & { ref: StateRef<Target> }

Parameters

NameTypeDefaultNote
targetHookTarget-The target element to observe
callbackUseResizeObserverCallback-The callback to execute when resize is detected

Returns

UseResizeObserverReturn

Contributors

ddebabin

Last updated on