Islands & Components API

Islands & Components API

Islands enable partial hydration, and control flow components help build reactive UIs.

Hydration API

hydrate

Register a component for hydration.

import { createSignal, hydrate } from '@luna_ui/luna';

interface CounterProps {
  initial: number;
}

function Counter(props: CounterProps) {
  const [count, setCount] = createSignal(props.initial);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count()}
    </button>
  );
}

hydrate("counter", Counter);

HTML Attributes

<div
  luna:id="counter"
  luna:url="/static/counter.js"
  luna:state='{"initial":5}'
  luna:client-trigger="load"
>
  <button>Count: 5</button>
</div>
AttributeDescription
luna:idComponent identifier
luna:urlJavaScript module URL
luna:stateSerialized props (JSON)
luna:client-triggerWhen to hydrate

hydrateWC

Register a Web Component for hydration with Shadow DOM.

import { createSignal, hydrateWC } from '@luna_ui/luna';

function Counter(props: { initial: number }) {
  const [count, setCount] = createSignal(props.initial);

  return (
    <>
      <style>{`:host { display: block; }`}</style>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count()}
      </button>
    </>
  );
}

hydrateWC("wc-counter", Counter);

Hydration Triggers

TriggerHTML ValueDescription
LoadloadImmediately on page load
IdleidleWhen browser is idle
VisiblevisibleWhen element enters viewport
Mediamedia:(query)When media query matches
NonenoneManual trigger only

Manual Hydration

// Trigger hydration programmatically
window.__LUNA_HYDRATE__?.("modal");

Control Flow Components

SolidJS-compatible control flow components.

For

Render a list of items.

import { createSignal, For } from '@luna_ui/luna';

const [items, setItems] = createSignal(['a', 'b', 'c']);

<For each={items}>
  {(item, index) => (
    <div>
      {index()}: {item}
    </div>
  )}
</For>

Signature

interface ForProps<T, U extends Node> {
  each: Accessor<T[]> | T[];
  fallback?: Node;
  children: (item: T, index: Accessor<number>) => U;
}

function For<T, U extends Node>(props: ForProps<T, U>): Node;

Index

Render a list with item getters (tracks by index, not reference).

import { createSignal, Index } from '@luna_ui/luna';

const [items, setItems] = createSignal(['a', 'b', 'c']);

<Index each={items}>
  {(itemGetter, index) => (
    <div>
      {index}: {itemGetter()}
    </div>
  )}
</Index>

Difference from For:

  • For - item is direct value, index is accessor

  • Index - item is accessor (getter), index is direct value

Show

Conditional rendering.

import { createSignal, Show } from '@luna_ui/luna';

const [isVisible, setIsVisible] = createSignal(false);

<Show when={isVisible} fallback={<div>Hidden</div>}>
  <div>Visible!</div>
</Show>

// With function children (receives truthy value)
const [user, setUser] = createSignal<User | null>(null);

<Show when={user}>
  {(u) => <div>Hello, {u.name}</div>}
</Show>

Signature

interface ShowProps<T> {
  when: T | Accessor<T>;
  fallback?: Node;
  children: Node | ((item: NonNullable<T>) => Node);
}

function Show<T>(props: ShowProps<T>): Node;

Switch / Match

Multi-branch conditional rendering.

import { createSignal, Switch, Match } from '@luna_ui/luna';

const [status, setStatus] = createSignal<'loading' | 'success' | 'error'>('loading');

<Switch fallback={<div>Unknown</div>}>
  <Match when={() => status() === 'loading'}>
    <div>Loading...</div>
  </Match>
  <Match when={() => status() === 'success'}>
    <div>Success!</div>
  </Match>
  <Match when={() => status() === 'error'}>
    <div>Error!</div>
  </Match>
</Switch>

Signature

interface MatchProps<T> {
  when: T | Accessor<T>;
  children: Node | ((item: NonNullable<T>) => Node);
}

interface SwitchProps {
  fallback?: Node;
  children: MatchResult<Node>[];
}

function Match<T>(props: MatchProps<T>): MatchResult<Node>;
function Switch(props: SwitchProps): Node;

Portal

Render children to a different DOM location.

import { Portal } from '@luna_ui/luna';

// Render to document.body (default)
<Portal>
  <div class="modal">Modal content</div>
</Portal>

// Render to specific selector
<Portal mount="#modal-root">
  <div class="modal">Modal content</div>
</Portal>

// Render with Shadow DOM encapsulation
<Portal useShadow>
  <div>Encapsulated content</div>
</Portal>

Signature

interface PortalProps {
  mount?: Element | string;  // Target element or CSS selector
  useShadow?: boolean;       // Use Shadow DOM
  children: Node | Node[] | (() => Node);
}

function Portal(props: PortalProps): Node;

Low-level APIs

import { portalToBody, portalToSelector, portalWithShadow } from '@luna_ui/luna';

// Portal to body
portalToBody([modalContent]);

// Portal to CSS selector
portalToSelector("#modal-root", [modalContent]);

// Portal with Shadow DOM
portalWithShadow([content]);

Provider

Provide context values to descendants.

import { createContext, useContext, Provider } from '@luna_ui/luna';

const ThemeContext = createContext('light');

<Provider context={ThemeContext} value="dark">
  <App />
</Provider>

// Inside App or descendants:
const theme = useContext(ThemeContext);  // 'dark'

DOM Utilities

mount / render

Mount a component to a DOM element.

import { mount, render, createElement, text } from '@luna_ui/luna';

// Using mount
mount(document.getElementById('app'), <App />);

// Using render (same as mount)
render(document.getElementById('app'), myComponent);

text / textDyn

Create text nodes.

import { text, textDyn, createSignal } from '@luna_ui/luna';

// Static text
const staticText = text("Hello");

// Dynamic text (reactive)
const [name, setName] = createSignal("Luna");
const dynamicText = textDyn(() => `Hello, ${name()}`);

show

Conditional rendering helper.

import { show, text, createSignal } from '@luna_ui/luna';

const [visible, setVisible] = createSignal(true);

const node = show(
  visible,
  () => text("Visible!")
);

forEach

Low-level list rendering.

import { forEach, text, createSignal } from '@luna_ui/luna';

const [items, setItems] = createSignal(['a', 'b', 'c']);

const list = forEach(
  items,
  (item, index) => text(`${index}: ${item}`)
);

events

Create event handler maps with method chaining.

import { events } from '@luna_ui/luna';

const handlers = events()
  .click((e) => console.log('clicked'))
  .input((e) => console.log('input'))
  .keydown((e) => console.log('keydown'));

useHost

Get the host element in a Web Component.

import { useHost, hydrateWC } from '@luna_ui/luna';

function Counter() {
  const host = useHost();

  const handleClick = () => {
    host.dispatchEvent(new CustomEvent('count-changed', {
      detail: { count: count() },
      bubbles: true,
    }));
  };

  return <button onClick={handleClick}>Click</button>;
}

hydrateWC("wc-counter", Counter);

Best Practices

Choose Appropriate Triggers

ContentRecommended Trigger
Above the fold, criticalload
Below the foldvisible
Analytics, non-criticalidle
Desktop-only featuresmedia
User-triggered (modals)none

Minimize Island Count

Fewer, larger islands are better than many small ones:

10 small islands = 10 script loads
2 larger islands = 2 script loads

Keep State Minimal

Only serialize what's needed:

// Good - minimal state
interface Props {
  userId: number;
  displayName: string;
}

// Bad - too much data
interface Props {
  user: FullUserObject;
  allSettings: CompleteSettings;
}

API Summary

Hydration

FunctionDescription
hydrate(id, component)Register component for hydration
hydrateWC(tagName, component)Register Web Component
useHost()Get host element in WC

Control Flow

ComponentDescription
ForList rendering by reference
IndexList rendering by index
ShowConditional rendering
Switch / MatchMulti-branch conditional
PortalRender to different location
ProviderProvide context values

DOM

FunctionDescription
mount(el, node)Mount to element
render(el, node)Render to element
text(content)Static text node
textDyn(getter)Dynamic text node
show(cond, render)Conditional node
forEach(items, render)List of nodes
events()Event handler builder