mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 19:32:43 +00:00
shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
This commit is contained in:
1 parent
6776366ce6
commit
6406208b35
637 files changed
+100297
-497
No files matched your search
@@ -0,0 +1,264 @@
|
||||
# Hydration, Frames, and Navigation
|
||||
|
||||
## What This Covers
|
||||
|
||||
How server-rendered UI becomes interactive in the browser, and how the page updates without a full navigation. Read this when the task involves:
|
||||
|
||||
- Marking a component for client-side hydration with `clientEntry`
|
||||
- Booting the client runtime with `run`
|
||||
- Streaming server content into a region of the page with `<Frame>` and reloading those regions
|
||||
- Triggering Navigation API transitions with `navigate(...)` or `link(...)`
|
||||
- Server rendering with `renderToStream` or `renderToString`
|
||||
- Managing the document `<head>`
|
||||
|
||||
For component-local state and updates, see `component-model.md`. For host-element behavior and events, see `mixins-styling-events.md`.
|
||||
|
||||
## Server First, Then Hydrate
|
||||
|
||||
Make the server route correct before adding `clientEntry(...)`. A POST should already do the right thing on its own — return HTML, a redirect, or an error response — and a GET should already render the page the user expects. `clientEntry` exists to layer interactivity on top of UI that already works without it.
|
||||
|
||||
When server state changes after a mutation, prefer reloading a `<Frame>` when the UI region already maps cleanly to a server-rendered route. Frames re-fetch the same route, so the rendering logic stays in one place and the client does not need a parallel "state" API.
|
||||
|
||||
```tsx
|
||||
on('submit', async (event, signal) => {
|
||||
event.preventDefault()
|
||||
await fetch(routes.cart.add.href(), {
|
||||
method: 'POST',
|
||||
body: new FormData(event.currentTarget),
|
||||
signal,
|
||||
})
|
||||
if (signal.aborted) return
|
||||
await handle.frames.get('cart-summary')?.reload()
|
||||
})
|
||||
```
|
||||
|
||||
Use polling or a small JSON state endpoint when the data changes outside this page, or when a tiny shared widget would be heavier to model as a frame. Pick the lightest sync mechanism that preserves clear ownership of rendering logic.
|
||||
|
||||
## Client Entries
|
||||
|
||||
Use `clientEntry` to mark a component for client-side hydration. In source-served apps, prefer the source module's `import.meta.url` as the entry ID and let server rendering map it to the public asset URL:
|
||||
|
||||
```tsx
|
||||
import { clientEntry, on, type Handle } from 'remix/ui'
|
||||
|
||||
export const Counter = clientEntry(
|
||||
import.meta.url,
|
||||
function Counter(handle: Handle<{ initialCount: number; label: string }>) {
|
||||
let count = handle.props.initialCount
|
||||
|
||||
return () => (
|
||||
<div>
|
||||
<span>
|
||||
{handle.props.label}: {count}
|
||||
</span>
|
||||
<button
|
||||
mix={on('click', () => {
|
||||
count++
|
||||
handle.update()
|
||||
})}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
On the server, provide `resolveClientEntry` to `renderToStream(...)` so source file URLs become browser-loadable asset URLs. Keep this resolution in the render helper so component modules do not hard-code deployment-specific asset paths:
|
||||
|
||||
```tsx
|
||||
let stream = renderToStream(<App />, {
|
||||
async resolveClientEntry(entryId, component) {
|
||||
let exportName = entryId.split('#')[1] || component.name
|
||||
if (!exportName) {
|
||||
throw new Error(`Unable to resolve client entry export for ${entryId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
href: await assetServer.getHref(entryId),
|
||||
exportName,
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
If the module export name differs from the component function name, include `#ExportName` in the entry ID or return the exact export name from `resolveClientEntry`. A render helper that only supports source-owned entries can also fail fast when `entryId` is not a `file://` URL.
|
||||
|
||||
On the server, `clientEntry` components render like any other component. The server wraps their output in comment markers and serializes props into a `<script type="application/json">` tag.
|
||||
|
||||
Client entry props must be serializable: strings, numbers, booleans, `null`, `undefined`, plain objects/arrays of the above, JSX elements, and `<Frame>` elements. Functions and class instances cannot be passed.
|
||||
|
||||
## Booting the Client
|
||||
|
||||
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
|
||||
|
||||
```tsx
|
||||
import { run } from 'remix/ui'
|
||||
|
||||
let app = run({
|
||||
async loadModule(moduleUrl, exportName) {
|
||||
let mod = await import(moduleUrl)
|
||||
return mod[exportName]
|
||||
},
|
||||
async resolveFrame(src, signal, target) {
|
||||
let headers = new Headers({ accept: 'text/html' })
|
||||
if (target) headers.set('x-remix-target', target)
|
||||
let response = await fetch(src, { headers, signal })
|
||||
return response.body ?? (await response.text())
|
||||
},
|
||||
})
|
||||
|
||||
app.addEventListener('error', (event) => {
|
||||
console.error('Component error:', event.error)
|
||||
})
|
||||
|
||||
await app.ready()
|
||||
```
|
||||
|
||||
### `run` options
|
||||
|
||||
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
|
||||
- **`resolveFrame(src, signal, target)`** (optional) — called when a `<Frame>` loads or reloads content. `target` is available when frame targeting matters.
|
||||
|
||||
### `app` methods
|
||||
|
||||
- **`app.ready()`** — resolves when all initial client entries are hydrated
|
||||
- **`app.flush()`** — synchronously flushes all pending updates
|
||||
- **`app.dispose()`** — tears down all hydrated components
|
||||
|
||||
`app` is an `EventTarget` that emits `error` events from any hydrated component.
|
||||
|
||||
## Frames
|
||||
|
||||
A `<Frame>` renders server content into the page. Frames stream after the initial HTML, nest inside other frames, contain client entries, and can be reloaded without full page navigation.
|
||||
|
||||
```tsx
|
||||
import { Frame } from 'remix/ui'
|
||||
|
||||
function App() {
|
||||
return () => (
|
||||
<div>
|
||||
<Frame src="/sidebar" fallback={<div>Loading...</div>} />
|
||||
<Frame name="main" src="/main-content" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Frame props
|
||||
|
||||
- **`src`** (required) — URL to fetch the frame content from
|
||||
- **`fallback`** (optional) — content to show while loading; determines streaming behavior
|
||||
- **`name`** (optional) — registers the frame for lookup via `handle.frames.get(name)`
|
||||
- **`on`** (optional) — event handlers for events dispatched from the frame element
|
||||
|
||||
### Blocking vs non-blocking
|
||||
|
||||
- **Without `fallback`** (blocking) — the server waits for frame content before sending the initial HTML chunk
|
||||
- **With `fallback`** (non-blocking) — the fallback renders immediately; real content streams in later and replaces it
|
||||
|
||||
### Reloading frames
|
||||
|
||||
Client entries inside a frame can trigger a reload:
|
||||
|
||||
```tsx
|
||||
// Reload the containing frame
|
||||
handle.frame.reload()
|
||||
|
||||
// Reload an adjacent named frame
|
||||
await handle.frames.get('cart-summary')?.reload()
|
||||
|
||||
// Reload the entire page/frame tree
|
||||
handle.frames.top.reload()
|
||||
```
|
||||
|
||||
When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state.
|
||||
|
||||
### Nested frames
|
||||
|
||||
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
|
||||
|
||||
## Server Rendering
|
||||
|
||||
### `renderToStream`
|
||||
|
||||
Renders a component tree to a `ReadableStream<Uint8Array>`. Sends initial HTML immediately and streams frame content as it resolves:
|
||||
|
||||
```tsx
|
||||
import { renderToStream } from 'remix/ui/server'
|
||||
|
||||
let stream = renderToStream(<App />, {
|
||||
frameSrc: request.url,
|
||||
resolveFrame(src, target, context) {
|
||||
let frameUrl = new URL(src, context?.currentFrameSrc ?? request.url)
|
||||
return fetchHtml(frameUrl)
|
||||
},
|
||||
onError(error) {
|
||||
console.error(error)
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||
})
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- **`frameSrc`** — seeds SSR frame state; populates `handle.frame.src` and `handle.frames.top.src`
|
||||
- **`topFrameSrc`** — overrides the root frame URL for nested frame renders (carry forward from `resolveFrame` context)
|
||||
- **`resolveFrame(src, target, context)`** — return HTML string, `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the containing frame URL; `context.topFrameSrc` is the outer document URL
|
||||
- **`onError(error)`** — called on rendering errors
|
||||
|
||||
### `renderToString`
|
||||
|
||||
Renders a component tree to a complete HTML string. Use for static pages or embedding HTML:
|
||||
|
||||
```tsx
|
||||
import { renderToString } from 'remix/ui/server'
|
||||
let html = await renderToString(<App />)
|
||||
```
|
||||
|
||||
### CSS in SSR
|
||||
|
||||
Components using the `css` mixin have styles collected during rendering and emitted as a single `<style>` tag in `<head>`. No client-side style injection needed.
|
||||
|
||||
## Navigation
|
||||
|
||||
Use real anchors for normal document navigation. For app-driven navigation:
|
||||
|
||||
- `navigate(href, options?)` — performs a Navigation API transition
|
||||
- `link(href, options?)` mixin — makes any element behave like a navigation link
|
||||
|
||||
```tsx
|
||||
import { navigate } from 'remix/ui'
|
||||
navigate('/dashboard', { history: 'replace' })
|
||||
```
|
||||
|
||||
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
|
||||
|
||||
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`.
|
||||
|
||||
## Head Management
|
||||
|
||||
Manage document head with an explicit `<head>` in your document structure:
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
return () => (
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard</title>
|
||||
<meta name="description" content="Team dashboard" />
|
||||
<link rel="stylesheet" href="/styles/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>...</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Put `title`, `meta`, `link`, and `style` tags inside an explicit `<head>`. Bare head-like tags rendered outside `<head>` stay where they are — they are not moved into the document head for you.
|
||||
Reference in new issue
Block a user