Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Headless Components -- Oddzielenie logiki od prezentacji

Wyobraz sobie, ze na statku kosmicznym masz uniwersalny modul sterowania, ktory mozna podlaczyc do dowolnego panelu -- ten sam modul obsluguje zarowno kokpit, jak i panel inzynierski, ale kazdy wyswietla dane na swoj sposob. To wlasnie idea Headless Components -- komponentow, ktore zawieraja cala logike, ale zero UI.

Czym sa Headless Components?

Headless Component to komponent lub hook, ktory:

  1. Zarzadza stanem i logika (otwieranie/zamykanie, selekcja, nawigacja)
  2. NIE renderuje zadnego JSX -- nie ma wlasnego wygladu
  3. Oddaje kontrole nad UI konsumentowi przez propsy, render props lub hooki
1// Headless Dropdown -- cala logika, zero UI
2function useDropdown(options) {
3  const [isOpen, setIsOpen] = useState(false);
4  const [selectedIndex, setSelectedIndex] = useState(-1);
5  const [selectedOption, setSelectedOption] = useState(null);
6
7  const toggle = () => setIsOpen(prev => !prev);
8  const close = () => setIsOpen(false);
9
10  const select = (index) => {
11    setSelectedIndex(index);
12    setSelectedOption(options[index]);
13    setIsOpen(false);
14  };
15
16  // Nawigacja klawiatura
17  const handleKeyDown = (e) => {
18    if (e.key === 'ArrowDown') {
19      setSelectedIndex(prev => Math.min(prev + 1, options.length - 1));
20    } else if (e.key === 'ArrowUp') {
21      setSelectedIndex(prev => Math.max(prev - 1, 0));
22    } else if (e.key === 'Enter' && selectedIndex >= 0) {
23      select(selectedIndex);
24    } else if (e.key === 'Escape') {
25      close();
26    }
27  };
28
29  return {
30    isOpen, selectedIndex, selectedOption,
31    toggle, close, select, handleKeyDown,
32    getToggleProps: () => ({
33      onClick: toggle,
34      onKeyDown: handleKeyDown,
35      'aria-expanded': isOpen,
36      'aria-haspopup': 'listbox',
37    }),
38    getOptionProps: (index) => ({
39      onClick: () => select(index),
40      'aria-selected': selectedIndex === index,
41      role: 'option',
42    }),
43  };
44}

Uzycie headless dropdown z dowolnym UI

Klucz do headless components -- ten sam hook, rozne wyglady:

1// Wersja 1: Prosty dropdown
2function SimpleDropdown({ options, label }) {
3  const dropdown = useDropdown(options);
4
5  return (
6    <div className="simple-dropdown">
7      <button {...dropdown.getToggleProps()}>
8        {dropdown.selectedOption || label}
9      </button>
10      {dropdown.isOpen && (
11        <ul role="listbox">
12          {options.map((opt, i) => (
13            <li key={i} {...dropdown.getOptionProps(i)}>
14              {opt}
15            </li>
16          ))}
17        </ul>
18      )}
19    </div>
20  );
21}
22
23// Wersja 2: Kosmiczny dropdown z ikonami
24function SpaceDropdown({ options, icons }) {
25  const dropdown = useDropdown(options);
26
27  return (
28    <div className="space-dropdown">
29      <div {...dropdown.getToggleProps()} className="space-trigger">
30        <span>{dropdown.selectedOption || 'Wybierz system'}</span>
31        <span>{dropdown.isOpen ? '▲' : '▼'}</span>
32      </div>
33      {dropdown.isOpen && (
34        <div className="space-menu">
35          {options.map((opt, i) => (
36            <div key={i} {...dropdown.getOptionProps(i)} className="space-option">
37              <span>{icons[i]}</span>
38              <span>{opt}</span>
39            </div>
40          ))}
41        </div>
42      )}
43    </div>
44  );
45}

Headless Toggle / Disclosure

Kolejny przyklad -- headless toggle do budowania accordionow, expandable sections, spoilerow:

1function useToggle(initialState = false) {
2  const [isOpen, setIsOpen] = useState(initialState);
3
4  return {
5    isOpen,
6    toggle: () => setIsOpen(p => !p),
7    open: () => setIsOpen(true),
8    close: () => setIsOpen(false),
9    getToggleProps: () => ({
10      onClick: () => setIsOpen(p => !p),
11      'aria-expanded': isOpen,
12    }),
13    getPanelProps: () => ({
14      role: 'region',
15      hidden: !isOpen,
16    }),
17  };
18}
19
20// Uzycie 1: Accordion
21function SystemDetails({ title, children }) {
22  const { isOpen, getToggleProps, getPanelProps } = useToggle();
23
24  return (
25    <div className="accordion-item">
26      <button {...getToggleProps()}>
27        {title} {isOpen ? '−' : '+'}
28      </button>
29      <div {...getPanelProps()}>
30        {isOpen && children}
31      </div>
32    </div>
33  );
34}
35
36// Uzycie 2: Tooltip
37function InfoTooltip({ content, children }) {
38  const { isOpen, getToggleProps, getPanelProps } = useToggle();
39
40  return (
41    <span className="tooltip-wrapper">
42      <span {...getToggleProps()} className="tooltip-trigger">
43        {children}
44      </span>
45      <div {...getPanelProps()} className="tooltip-content">
46        {isOpen && content}
47      </div>
48    </span>
49  );
50}

Headless Tabs

1function useTabs(tabCount, defaultTab = 0) {
2  const [activeTab, setActiveTab] = useState(defaultTab);
3
4  return {
5    activeTab,
6    setActiveTab,
7    getTabProps: (index) => ({
8      onClick: () => setActiveTab(index),
9      role: 'tab',
10      'aria-selected': activeTab === index,
11      tabIndex: activeTab === index ? 0 : -1,
12    }),
13    getPanelProps: (index) => ({
14      role: 'tabpanel',
15      hidden: activeTab !== index,
16    }),
17    isActive: (index) => activeTab === index,
18  };
19}

Dlaczego headless?

| Cecha | Zwykly komponent | Headless Component | |-------|-----------------|-------------------| | UI | Wbudowane style/JSX | Zero -- TY decydujesz | | Reuzywalnalnosc | Ograniczona do jednego wygladu | Nieograniczona | | Accessibility | Trzeba implementowac recznie | Wbudowane aria-atrybuty | | Testowalnosc | Wymaga renderowania | Testuj sam hook |

Headless UI w ekosystemie React

Biblioteka @headlessui/react (od tworcow Tailwind CSS) dostarcza gotowe headless komponenty:

  • Listbox
    -- dropdown z pelna obsluga klawiatury
  • Combobox
    -- autocomplete/search
  • Dialog
    -- modal z focus trap
  • Disclosure
    -- accordion/expandable
  • Menu
    -- dropdown menu
  • Tabs
    -- zakladki

Kazdy z nich dostarcza logike, aria-atrybuty i obsluge klawiatury, ale zero stylowania. Ty dostarczasz caly wyglad.

Kiedy uzywac headless components?

  • Budujesz design system -- rozne zespoly potrzebuja tych samych interakcji, ale roznych stylów
  • Potrzebujesz pelnej dostepnosci (a11y) -- headless komponenty maja wbudowane aria-atrybuty
  • Tworzysz komponenty uzywane w wielu projektach -- logika ta sama, style rozne
  • Chcesz oddzielic odpowiedzialnosc -- hook = logika, komponent = wyglad

Headless Components to jak uniwersalny system nawigacyjny statku kosmicznego -- jeden modul sterowania, ktory mozna podlaczyc do dowolnego panelu wyswietlania. Logika jest ta sama, ale kazdy panel wyglada inaczej, bo kazda stacja ma inne potrzeby!

Vai a CodeWorlds