Создание собственных примитивов

Radix UI предоставляет набор готовых примитивов для построения пользовательских интерфейсов, однако иногда возникает необходимость создавать собственные примитивы с уникальной логикой и поведением. Собственные примитивы позволяют сохранить консистентность интерфейса, управлять состоянием компонентов и интегрировать сторонние библиотеки.

Структура примитива

Примитив в Radix UI состоит из нескольких ключевых элементов:

  1. Контейнер (Root) — главный компонент, который управляет состоянием и связывает все части примитива.
  2. Триггер (Trigger) — элемент, который инициирует изменения состояния.
  3. Контент (Content) — визуальная часть, отображаемая в ответ на действия пользователя.
  4. Порталы (Portal) — используются для рендеринга контента вне текущего DOM-контекста, что помогает обходить проблемы с z-index и overflow.
  5. Hooks и Context — обеспечивают связь между компонентами и управление состоянием.

Пример создания примитива Toggle

Для демонстрации создадим собственный примитив переключателя состояния.

import React, { createContext, useContext, useState } from "react";

const ToggleContext = createContext();

export function ToggleRoot({ children, defaultOpen = false }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <ToggleContext.Provider value={{ open, setOpen }}>
      {children}
    </ToggleContext.Provider>
  );
}

export function ToggleTrigger({ children }) {
  const { open, setOpen } = useContext(ToggleContext);
  return (
    <button onCl ick={() => setOpen(!open)}>
      {children} {open ? "Включено" : "Выключено"}
    </button>
  );
}

export function ToggleContent({ children }) {
  const { open } = useContext(ToggleContext);
  if (!open) return null;
  return <div>{children}</div>;
}

В этом примере:

  • ToggleRoot управляет состоянием open через useState и предоставляет его дочерним компонентам через контекст.
  • ToggleTrigger реагирует на клики и меняет состояние.
  • ToggleContent отображается только тогда, когда состояние open истинно.

Использование порталов для примитивов

Для сложных интерфейсов с модальными окнами или всплывающими подсказками необходимо использовать порталы, чтобы избежать проблем с CSS:

import { createPortal } from "react-dom";

export function TogglePortal({ children }) {
  return createPortal(
    <div className="toggle-portal">{children}</div>,
    document.body
  );
}

Интеграция портала в примитив позволяет выводить контент поверх других элементов, сохраняя логику управления состоянием через контекст.

Управление сложными состояниями

Для примитивов с несколькими уровнями взаимодействия удобно использовать compound components с контекстом:

const DropdownContext = createContext();

export function Dropdown({ children }) {
  const [open, setOpen] = useState(false);
  return (
    <DropdownContext.Provider value={{ open, setOpen }}>
      <div className="dropdown">{children}</div>
    </DropdownContext.Provider>
  );
}

export function DropdownTrigger({ children }) {
  const { setOpen } = useContext(DropdownContext);
  return <button onCl ick={() => setOpen(prev => !prev)}>{children}</button>;
}

export function DropdownContent({ children }) {
  const { open } = useContext(DropdownContext);
  return open ? <div className="dropdown-content">{children}</div> : null;
}

Ключевые моменты:

  • Использование контекста позволяет хранить состояние в одном месте и избежать передачи пропсов через множество уровней.
  • Примитивы остаются гибкими и могут быть расширены дополнительными хендлерами событий или стилями.

Расширение стандартных примитивов Radix UI

Radix UI позволяет создавать собственные примитивы на основе существующих, расширяя их функционал:

import * as CheckboxPrimitive from "@radix-ui/react-checkbox";

export function CustomCheckbox({ label, ...props }) {
  return (
    <CheckboxPrimitive.Root {...props}>
      <CheckboxPrimitive.Indicator />
      {label && <span>{label}</span>}
    </CheckboxPrimitive.Root>
  );
}

Расширение стандартного примитива сохраняет совместимость с API Radix и позволяет добавлять кастомную визуализацию и логику.

Практические рекомендации

  • Использовать контекст для управления состоянием примитива и связи между его частями.
  • Выносить визуальные элементы в отдельные компоненты для переиспользования.
  • При необходимости рендерить контент поверх других элементов использовать порталы.
  • Создавать compound components для сложных UI-конструкций, чтобы сохранить единый источник правды для состояния.
  • Расширять стандартные примитивы Radix UI, а не переписывать их полностью, чтобы сохранить совместимость и доступные accessibility-фичи.

Работа с доступностью

Каждый собственный примитив должен учитывать accessibility (ARIA) атрибуты, фокусировку и keyboard navigation. Примитивы Radix UI предоставляют удобные хуки и вспомогательные компоненты для этих целей, которые можно интегрировать в собственные примитивы.

<button aria-pressed={open}>Toggle</button>

Правильная работа с ARIA и фокусом обеспечивает совместимость с экранными читалками и улучшает UX для пользователей с ограниченными возможностями.

Заключение по подходу

Создание собственных примитивов в Radix UI строится вокруг концепции управления состоянием через контекст, разделения на root, trigger и content, и применения порталов для визуальных компонентов. Такой подход обеспечивает гибкость, переиспользуемость и соответствие стандартам доступности.