Utils

Пакет с хуками и хелперами для разработки React-компонентов: controlled/uncontrolled состояние, дебаунс, свайпы, модалки, персист данных, работа с пропами и SSR.

Оформление (тема, бренд, плотность, цветовая схема) переехало в @cloud-ru/ds-theme — см. Theme и паттерн Оформление.

Установка

pnpm add @cloud-ru/ds-utils

Примеры использования

useValueControl

Поддержка controlled/uncontrolled: один интерфейс для value/defaultValue и onChange.

import { useValueControl } from '@cloud-ru/ds-utils';

function Toggle({ value, defaultValue, onChange }) {
  const [open, setOpen] = useValueControl({ value, defaultValue, onChange });
  return (
    <button type="button" onClick={() => setOpen(!open)}>
      {open ? 'Закрыть' : 'Открыть'}
    </button>
  );
}

useDebounce

Возвращает стабильный коллбек, который вызывается с задержкой после последнего вызова.

import { useDebounce } from '@cloud-ru/ds-utils';

function Search() {
  const handleSearch = useDebounce(() => {
    console.log('Поиск по запросу...');
  }, 300);
  return <input onChange={handleSearch} />;
}

useEventHandler

Возвращает обработчик с неизменной ссылкой: внутри всегда вызывается актуальная функция, без лишних ре-рендеров дочерних компонентов.

import { useEventHandler } from '@cloud-ru/ds-utils';

function List({ items, onItemClick }) {
  const handleClick = useEventHandler(onItemClick);
  return items.map((item) => (
    <div key={item.id} onClick={handleClick} role="presentation">
      {item.name}
    </div>
  ));
}

useLayoutEffect (SSR-безопасный)

Из пакета экспортируется useLayoutEffect: в браузере это обычный useLayoutEffect, в SSR — useEffect.

import { useRef } from 'react';
import { useLayoutEffect } from '@cloud-ru/ds-utils';

function Measure() {
  const ref = useRef(null);
  useLayoutEffect(() => {
    if (ref.current) console.log(ref.current.getBoundingClientRect());
  }, []);
  return <div ref={ref}>...</div>;
}

useDynamicList

Делит список на видимые и скрытые элементы по ширине контейнера.

import { useRef } from 'react';
import { useDynamicList } from '@cloud-ru/ds-utils';

function Tabs({ items }) {
  const containerRef = useRef(null);
  const { visibleItems, hiddenItems } = useDynamicList({
    items,
    parentContainerRef: containerRef,
    maxVisibleItems: 5,
  });
  return (
    <div ref={containerRef}>
      {visibleItems.map((item) => (
        <span key={item.id}>{item.label}</span>
      ))}
      {hiddenItems.length > 0 && <span>…</span>}
    </div>
  );
}

useSwipeable

Обработка свайпов (на базе react-swipeable).

import { useSwipeable } from '@cloud-ru/ds-utils';

function SwipeCard() {
  const handlers = useSwipeable({
    onSwipedLeft: () => console.log('свайп влево'),
    onSwipedRight: () => console.log('свайп вправо'),
    availableDirections: ['LEFT', 'RIGHT'],
  });
  return <div {...handlers}>Свайпни меня</div>;
}

useModalOpenState

Подключает к модалке закрытие по кнопке «Назад» (popstate) и по CloseWatcher.

import { useModalOpenState } from '@cloud-ru/ds-utils';

function Modal({ open, onClose }) {
  useModalOpenState(open, onClose, {
    closeOnPopstate: true,
    closeByCloseWatcher: true,
  });
  return open ? <div role="dialog">...</div> : null;
}

usePopstateSubscription

Подписка на событие popstate.

import { usePopstateSubscription } from '@cloud-ru/ds-utils';

function SyncWithHistory() {
  usePopstateSubscription((e) => {
    console.log('Навигация назад/вперёд', e.state);
  }, true);
  return null;
}

useDataPersist

Чтение и запись состояния в localStorage и в query-параметрах URL.

import { useState } from 'react';
import { useDataPersist } from '@cloud-ru/ds-utils';

const options = {
  queryKey: 'filter',
  localStorageKey: 'my-filter',
  validateData: (v): v is { query: string } => typeof v?.query === 'string',
};

function FilterState() {
  const { getDefaultData, setDataToStorages } = useDataPersist({ options });
  const [filter, setFilter] = useState(getDefaultData ?? { query: '' });
  const save = () => setDataToStorages(filter);
  return <input value={filter.query} onChange={(e) => setFilter({ query: e.target.value })} onBlur={save} />;
}

excludeSupportProps, extractSupportProps, extractDataTestProps

import { excludeSupportProps, extractSupportProps, extractDataTestProps } from '@cloud-ru/ds-utils';

const props = {
  'data-test-id': 'submit-btn',
  'aria-label': 'Отправить',
  onClick: () => {},
  className: 'btn',
};

const restProps = excludeSupportProps(props);
const supportProps = extractSupportProps(props);
const dataTestProps = extractDataTestProps(props);

isBrowser

import { isBrowser } from '@cloud-ru/ds-utils';

if (isBrowser()) {
  window.addEventListener('resize', handler);
}

Типы: ValueOf, WithSupportProps

import type { ValueOf, WithSupportProps } from '@cloud-ru/ds-utils';

const sizes = { sm: 'small', md: 'medium', lg: 'large' } as const;
type Size = ValueOf<typeof sizes>;

type ButtonProps = WithSupportProps<{
  label: string;
  variant: 'primary' | 'secondary';
}>;

Storybook

Интерактивные примеры — раздел Utils в локальном Storybook (pnpm dev:storybook).