Real-world architecture patterns for Astro 5 static shells with dynamic SolidJS micro-islands.
Production architecture patterns for embedding reactive SolidJS micro-islands inside Astro 5 static templates.
Astro 5 Island Integration
Mount reactive Tide micro-islands inside static Astro pages using client directives:
---
// src/pages/dashboard.astro
import BaseLayout from "../layouts/BaseLayout.astro";
import { DashboardMetricsIsland } from "../components/DashboardIsland";
---
<BaseLayout title="System Dashboard">
<div class="max-w-7xl mx-auto px-4 py-8">
<h1 class="text-2xl font-bold">Local Agent Dashboard</h1>
<!-- Client directive delays hydration until idle -->
<DashboardMetricsIsland client:idle />
</div>
</BaseLayout>
URL Shorthand with Dynamic ETag Verification
Enable etag: true to send standard If-None-Match HTTP headers. When the server responds with 304 Not Modified, Tide avoids parsing JSON and skips re-rendering.
import { createTide } from '@uikode/tide';
export function AgentStatusTile() {
const agent = createTide<{ status: string; uptime: number }>({
key: 'agent-status',
url: '/api/agent/status',
etag: true,
pollInterval: 15_000,
});
return (
<div class="metric-tile">
<span>Status: {agent.data()?.status}</span>
</div>
);
}
Content Deduplication via hashCompare
hashCompare: true calculates a lightweight 32-bit djb2 hash over the response payload. If the received data matches the current state, Tide skips updating the SolidJS signal, preventing downstream virtual DOM reconciliation.
const stack = createTide({
key: 'stack-topology',
url: '/api/stack/status',
hashCompare: true,
pollInterval: 5_000,
});
Reactive Key for Filter & Tab Switching
When using dynamic getter functions for key and url, Tide maintains independent cache slots for each parameter combination.
import { createSignal, For, Show } from 'solid-js';
import { createTide } from '@uikode/tide';
export function AnalyticsViewer() {
const [range, setRange] = createSignal<'24h' | '7d' | '30d'>('24h');
const chart = createTide<Array<{ timestamp: number; value: number }>>({
key: () => `metrics-chart-${range()}`,
url: () => `/api/metrics?range=${range()}`,
staleTime: 30_000,
cacheTime: 300_000,
persist: true,
});
return (
<div>
<div class="flex gap-2 mb-4">
<For each={['24h', '7d', '30d'] as const}>
{(tab) => (
<button
class={range() === tab ? 'bg-primary text-white' : 'bg-secondary'}
onClick={() => setRange(tab)}
>
{tab}
</button>
)}
</For>
</div>
<Show when={!chart.loading()} fallback={<p>Loading chart data...</p>}>
<pre>{JSON.stringify(chart.data(), null, 2)}</pre>
</Show>
</div>
);
}
Lifecycle Control with pause() and resume()
When applying optimistic mutations, call tide.pause() to prevent background polling from overwriting the pending state. Once the mutation resolves, call tide.resume() and tide.refetch().
async function handleUpdateStatus(newStatus: string) {
tide.pause(); // Pause background polling
tide.mutate((prev) => (prev ? { ...prev, status: newStatus } : null));
try {
await fetch('/api/status', {
method: 'POST',
body: JSON.stringify({ status: newStatus }),
});
} finally {
tide.resume();
await tide.refetch();
}
}