Remix интеграция

Для интеграции Radix UI с приложением на Remix необходимо сначала установить базовые пакеты. Основным пакетом является @radix-ui/react-*, где * обозначает конкретный компонент (Dialog, Popover, DropdownMenu и др.). Обычно вместе с Radix используют стилизаторы, например Stitches, но можно обойтись и обычным CSS или Tailwind.

Пример установки для Dialog и Popover:

npm install @radix-ui/react-dialog @radix-ui/react-popover
npm install @stitches/react

После установки создаётся базовая структура проекта Remix, где компоненты Radix можно использовать в UI-подкомпонентах (components/) или непосредственно в страницах (routes/).


Структура компонентов

Radix UI предоставляет контролируемые и неконтролируемые компоненты.

  • Контролируемые компоненты требуют управления состоянием извне через state и setState.
  • Неконтролируемые компоненты используют внутреннее управление состоянием, что упрощает быстрые прототипы.

Пример использования Dialog с контролируемым состоянием:

import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react';

export default function MyDialog() {
  const [open, setOpen] = useState(false);

  return (
    <Dialog.Root open={open} onOpenCha nge={setOpen}>
      <Dialog.Trigger>Открыть диалог</Dialog.Trigger>
      <Dialog.Overlay className="overlay" />
      <Dialog.Content className="dialog-content">
        <Dialog.Title>Заголовок</Dialog.Title>
        <Dialog.Description>Описание диалога</Dialog.Description>
        <button onCl ick={() => setOpen(false)}>Закрыть</button>
      </Dialog.Content>
    </Dialog.Root>
  );
}

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

  • Dialog.Root управляет состоянием открытия/закрытия.
  • Dialog.Trigger — элемент, который инициирует открытие.
  • Dialog.Overlay обеспечивает затемнение фона.
  • Dialog.Content содержит весь контент диалога.

Интеграция с маршрутизацией Remix

Radix UI компоненты работают как обычные React-компоненты, поэтому их можно встраивать в страницы Remix (routes/) и использовать вместе с loader и action.

Например, диалог для подтверждения действия с формой Remix:

import * as Dialog from '@radix-ui/react-dialog';
import { Form } from '@remix-run/react';

export default function DeleteDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Удалить запись</Dialog.Trigger>
      <Dialog.Overlay className="overlay" />
      <Dialog.Content className="dialog-content">
        <Dialog.Title>Подтвердите удаление</Dialog.Title>
        <Form method="post">
          <button type="submit">Удалить</button>
        </Form>
      </Dialog.Content>
    </Dialog.Root>
  );
}

Особенности взаимодействия:

  • Form из Remix может быть встроен в любой Radix UI компонент без ограничений.
  • Можно использовать action для серверного удаления данных, а состояние диалога управлять локально через onOpenChange.

Стилизация компонентов

Radix UI поставляется как unstyled components, что даёт полную свободу в дизайне. Для интеграции с Remix популярны два подхода:

1. Tailwind CSS

<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded shadow-lg">
  <Dialog.Title className="text-lg font-bold mb-2">Заголовок</Dialog.Title>
</Dialog.Content>

2. Stitches

import { createStitches } from '@stitches/react';
import * as Dialog from '@radix-ui/react-dialog';

const { styled } = createStitches({
  variants: {
    size: {
      small: { width: 300 },
      large: { width: 600 },
    }
  }
});

const DialogContent = styled(Dialog.Content, {
  backgroundColor: 'white',
  borderRadius: '8px',
  padding: '20px',
  boxShadow: '0 10px 15px rgba(0,0,0,0.1)',
});

export default function MyDialog() {
  return (
    <Dialog.Root>
      <Dialog.Trigger>Открыть</Dialog.Trigger>
      <DialogContent size="large">
        <Dialog.Title>Заголовок</Dialog.Title>
      </DialogContent>
    </Dialog.Root>
  );
}

Преимущество: лёгкая настройка вариаций компонентов через CSS-in-JS, без изменения логики компонента.


Использование Portals в Remix

Radix UI использует ReactDOM.createPortal для рендеринга некоторых компонентов (Popover, Dialog, Tooltip) вне обычного DOM-дерева. В Remix это работает без дополнительной настройки, но важно учитывать:

  • На сервере порталы не рендерятся, поэтому необходимо условно рендерить компоненты только на клиенте.
  • Используется проверка typeof window !== 'undefined'.

Пример условного рендеринга:

import { useEffect, useState } from 'react';
import * as Dialog from '@radix-ui/react-dialog';

export default function ClientOnlyDialog() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  if (!mounted) return null;

  return (
    <Dialog.Root>
      <Dialog.Trigger>Открыть</Dialog.Trigger>
      <Dialog.Content>Контент</Dialog.Content>
    </Dialog.Root>
  );
}

Управление фокусом и доступностью

Radix UI автоматически обрабатывает фокус и клавиатурную навигацию для всех интерактивных компонентов:

  • Dialog блокирует фокус внутри диалога и возвращает его к триггеру после закрытия.
  • Popover и DropdownMenu корректно управляют фокусом при навигации с клавиатуры.
  • Все компоненты поддерживают атрибуты ARIA для улучшенной доступности.

Для Remix это особенно важно, так как серверный рендеринг не вмешивается в поведение клавиатурной навигации на клиенте.


Совместное использование с состоянием Remix

Radix UI компоненты можно сочетать с глобальным состоянием через useLoaderData, useActionData или useTransition:

import * as Dialog from '@radix-ui/react-dialog';
import { useTransition } from '@remix-run/react';

export default function SubmitDialog() {
  const transition = useTransition();

  return (
    <Dialog.Root>
      <Dialog.Trigger>Отправить данные</Dialog.Trigger>
      <Dialog.Content>
        {transition.state === 'submitting' ? (
          <p>Идёт отправка...</p>
        ) : (
          <button type="submit">Отправить</button>
        )}
      </Dialog.Content>
    </Dialog.Root>
  );
}

Особенности:

  • Состояние transition.state позволяет отслеживать процесс отправки данных в формах Remix.
  • Radix UI компоненты остаются независимыми, сохраняя доступность и управление фокусом.

Использование Radix UI с динамическими данными

Компоненты, такие как Select или DropdownMenu, удобно использовать с данными, загруженными через loader:

import * as Sel ect from '@radix-ui/react-select';
import { useLoaderData } fr om '@remix-run/react';

export function loader() {
  return [
    { id: 1, name: 'Первый элемент' },
    { id: 2, name: 'Второй элемент' },
  ];
}

export default function DynamicSelect() {
  const options = useLoaderData();

  return (
    <Select.Root>
      <Select.Trigger>Выберите элемент</Select.Trigger>
      <Select.Content>
        {options.map(option => (
          <Select.Item key={option.id} value={option.id.toString()}>
            <Select.ItemText>{option.name}</Select.ItemText>
          </Select.Item>
        ))}
      </Select.Content>
    </Select.Root>
  );
}

Особенности серверного рендеринга

Remix выполняет серверный рендеринг (SSR), но Radix UI компоненты рассчитаны на клиентскую работу. Необходимо учитывать:

  • Любой компонент, использующий Portal или анимации, должен рендериться только на клиенте.
  • Для предотвращения ошибок при SSR используют useEffect или отдельные ClientOnly обёртки.
  • Неконтролируемые компоненты могут требовать начального состояния defaultOpen или defaultValue для корректного SSR.

Эта структура обеспечивает гибкую интеграцию Radix UI в проекты Remix, с поддержкой серверного рендеринга, динамических данных, управления состоянием и кастомной стилизации.