--- url: /guide/7guis.md --- # 7 GUIs To verify that Vue Lynx behaves identically to Vue in real GUI scenarios, we forked the [7 GUIs](https://eugenkiss.github.io/7guis/) examples from the [official Vue.js documentation](https://vuejs.org/examples/#7-guis-counter) — seven classic tasks that cover typical challenges in GUI programming, from a simple counter to a full spreadsheet. ## Examples ### 1. Counter A simple button that increments a counter. Tests basic state management and event handling. ### 2. Temperature Converter Bidirectional conversion between Celsius and Fahrenheit. Tests two-way data flow and constraint handling. ### 3. Flight Booker A flight booking form with conditional validation. Tests constraints, conditional visibility, and input validation. ### 4. Timer An elapsed-time gauge with adjustable duration. Tests concurrency, competing user/signal interactions, and continuous updates. ### 5. CRUD A filterable list with create, read, update, and delete operations. Tests managing a dynamic collection of data. ### 6. Circle Drawer Draw circles on a canvas with undo/redo support. Tests custom drawing, a non-trivial undo model, and dialog control flow. ### 7. Cells A simple spreadsheet with formula evaluation. Tests change propagation, cell references, and reactive computation. --- url: /guide/ai-chat.md --- # AI Chat A full-featured AI chatbot ported feature-by-feature from the official [Nuxt AI Chatbot template](https://github.com/nuxt-ui-templates/chat) (Nuxt UI + Vercel AI SDK) — probably the most demanding "real-world" validation of Vue Lynx so far: streaming responses with thinking/reasoning, markdown with highlighted code blocks and tables, tool-call cards (weather, charts, web-search sources), chat history with date grouping, votes, message editing, share/rename/delete flows, a model picker, persisted runtime theming with light/dark mode, attachments, fuzzy command-palette search, and a responsive mobile layout with a slide-over sidebar. Try the quick prompts below — *"What is the weather in Bordeaux?"* streams a reasoning section and a weather card; *"Show me a chart of sales data"* renders an SVG line chart; *"Help me create a Vue composable"* shows markdown with highlighted code. Scan the QR code in the native tab to run the same bundle in [LynxExplorer](https://lynxjs.org/guide/start/quick-start.html). The playground above runs fully self-contained: the app probes for its API server and, when unreachable, falls back to an in-app demo backend with the same seeded history and deterministic mock AI streams. When you run the example locally with `pnpm dev:server`, the same UI talks to a standalone Node server that reimplements the Nuxt template's API routes — and streams **real models** through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) if you set `AI_GATEWAY_API_KEY`. ## Native chat techniques A polished chat send is not one animation. It is a handoff between the composer, message list, keyboard, and streaming response. [Vercel’s account of building the v0 iOS app](https://vercel.com/blog/how-we-built-the-v0-ios-app) inspired us to describe the work through those visible moments, then trace each moment back to the system that makes it possible. ### The first send establishes the stage Before an assistant message exists, the app captures the composer and viewport geometry. The outgoing text stays in a temporary animation layer while the app commits the real message, scroll position, and response space. The assistant reveal begins only after that update, so there is no target-position flash. The launch distance depends on the bubble height, viewport, composer, and keyboard. We stage the bubble at the captured composer position, commit layout, then start its transform on a later frame. Layout and animation never compete for the same first frame. ### The second send is the real test Repeat sends expose mistakes that the empty state hides. Native aligns the new user turn at the top, but keeps the previous turn stable until the moving bubble reaches its target. Vue Lynx [`nextTick()`](https://vue.lynxjs.org/guide/api/vue-lynx/Function.nextTick) waits for pending operations to reach the main thread before [`scrollIntoView`](https://lynxjs.org/api/elements/built-in/scroll-view#scrollintoview) performs the final alignment. Only then does the assistant stream appear. Web uses a different policy because it does not have the same native positioning guarantees: observed `scroll-top` updates keep the message list pinned to the bottom during sends and streaming. The interaction stays familiar even when the mechanism changes. ### The keyboard participates in layout Lynx [`` elements do not avoid the keyboard automatically](https://lynxjs.org/api/elements/built-in/input#keyboard-avoidance). The composer remains absolutely positioned at the bottom and listens for the [`keyboardstatuschanged` global event](https://lynxjs.org/api/lynx-api/event/global-event#keyboardstatuschanged). It applies the reported keyboard height with `setNativeProps`; the message list follows only if it was already following new output. ### Gesture feedback stays on the main thread Drawer gestures and press feedback use Vue Lynx [Main Thread Script](https://vue.lynxjs.org/guide/main-thread-script). Functions marked with `'main thread'` update element styles from `main-thread-bindtouch*` events without a background-thread round trip. The regular `tap` handler still performs the application action on Vue’s background thread. ## Porting notes The port keeps the original's data model (AI SDK v5 `UIMessage` parts over the UI-message-stream protocol) and nearly all of its composable/page logic, while re-implementing the DOM-coupled layers with Lynx elements: | Aspect | Nuxt original | Vue Lynx | |--------|---------------|----------| | **UI kit** | @nuxt/ui v4 (`UChat*`, `UDashboard*`, Reka UI portals) | Hand-built Lynx components (`view`/`text`/`image`/`scroll-view`/`input`) with Nuxt UI's semantic design tokens | | **Chat state** | `@ai-sdk/vue` `useChat` | Custom `useChat` speaking the same protocol (SSE on web; incremental polling + cancellation on native) | | **Markdown** | Comark + Shiki (HTML) | Markdown, including tables, → native-node renderer + lightweight code tokenizer | | **Charts** | nuxt-charts / Unovis (SVG DOM) | Chart generated as an inline SVG string for the native `` element | | **Dropdowns / modals** | Anchored popovers, portals | Action sheets + centered modals at app root | | **Theming** | Tailwind v4 + `app.config` | Runtime `--ui-*` CSS variables via inline vars (17 primary × 5 neutral picker included) | | **Responsive** | Tailwind `lg:` breakpoint | `SystemInfo.pixelWidth / pixelRatio` branch (no media queries on Lynx) | The example directory contains a complete [PRD with per-feature parity status](https://github.com/huxpro/vue-lynx/blob/main/examples/ai-chat/PRD.md) (65 features ported/adapted, 10 skipped with reasons), a [PORTING.md](https://github.com/huxpro/vue-lynx/blob/main/examples/ai-chat/PORTING.md) documenting what was reused vs rewritten plus the platform quirks discovered along the way, and [side-by-side screenshot comparisons](https://github.com/huxpro/vue-lynx/blob/main/examples/ai-chat/screenshots/COMPARISON.md) against the live original captured on Lynx for Web. --- url: /guide/api/plugin/Function.pluginVueLynx.md --- [vue-lynx/plugin](/guide/api/plugin/index.md) / pluginVueLynx # Function: pluginVueLynx() ```ts function pluginVueLynx(options): RsbuildPlugin[] ``` Create rsbuild / rspeedy plugins for Vue-Lynx dual-thread rendering. Returns an array of two plugins: 1. `@rsbuild/plugin-vue` — Vue SFC support (rspack-vue-loader + VueLoaderPlugin) 2. `lynx:vue` — Lynx dual-thread entry splitting, PAPI bootstrap, and CSS handling ## Parameters | Parameter | Type | | ------ | ------ | | `options` | [`PluginVueLynxOptions`](/guide/api/plugin/Interface.PluginVueLynxOptions.md) | ## Returns `RsbuildPlugin`\[] ## Defined in index.ts:206 --- url: /guide/api/plugin/Interface.PluginVueLynxOptions.md --- [vue-lynx/plugin](/guide/api/plugin/index.md) / PluginVueLynxOptions # Interface: PluginVueLynxOptions Options for [pluginVueLynx](/guide/api/plugin/Function.pluginVueLynx.md). ## Properties | Property | Type | Default value | Description | Defined in | | ------ | ------ | ------ | ------ | ------ | | `optionsApi?` | `boolean` | `true` | Whether to enable Vue's Options API support. Disabling it reduces bundle size. | index.ts:55 | | `prodDevtools?` | `boolean` | `false` | Whether to enable Vue devtools in production builds. | index.ts:61 | | `enableCSSSelector?` | `boolean` | `true` | Whether to enable CSS selector support in the Lynx template. When enabled, CSS from Vue `(): StrictUnwrapSlotsType> ``` ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `S` *extends* `Record`\<`string`, `any`> | `Record`\<`string`, `any`> | ## Returns `StrictUnwrapSlotsType`\<[`SlotsType`](/guide/api/vue-lynx/TypeAlias.SlotsType.md)\<`S`>> ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:274 --- url: /guide/api/vue-lynx/Function.effectScope.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / effectScope # Function: effectScope() ```ts function effectScope(detached?): EffectScope ``` Creates an effect scope object which can capture the reactive effects (i.e. computed and watchers) created within it so that these effects can be disposed together. For detailed use cases of this API, please consult its corresponding [RFC](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md). ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `detached`? | `boolean` | Can be used to create a "detached" effect scope. | ## Returns `EffectScope` ## See [https://vuejs.org/api/reactivity-advanced.html#effectscope](https://vuejs.org/api/reactivity-advanced.html#effectscope) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:685 --- url: /guide/api/vue-lynx/Function.getCurrentInstance.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / getCurrentInstance # Function: getCurrentInstance() ```ts function getCurrentInstance(): null | ComponentInternalInstance ``` ## Returns `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1472 --- url: /guide/api/vue-lynx/Function.getCurrentScope.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / getCurrentScope # Function: getCurrentScope() ```ts function getCurrentScope(): EffectScope | undefined ``` Returns the current active effect scope if there is one. ## Returns `EffectScope` | `undefined` ## See [https://vuejs.org/api/reactivity-advanced.html#getcurrentscope](https://vuejs.org/api/reactivity-advanced.html#getcurrentscope) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:691 --- url: /guide/api/vue-lynx/Function.guardReactiveProps.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / guardReactiveProps # Function: guardReactiveProps() ```ts function guardReactiveProps(props): Data & VNodeProps | null ``` ## Parameters | Parameter | Type | | ------ | ------ | | `props` | `null` | `Data` & `VNodeProps` | ## Returns `Data` & `VNodeProps` | `null` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1293 --- url: /guide/api/vue-lynx/Function.h.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / h # Function: h() ## h(type, children) ```ts function h(type, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `K` *extends* keyof `HTMLElementTagNameMap` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `K` | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `K` *extends* keyof `HTMLElementTagNameMap` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `K` | | `props`? | `null` | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `HTMLElementEventHandler` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | | `props`? | `null` | `RawProps` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | *typeof* [`Text`](/guide/api/vue-lynx/Variable.Text.md) | *typeof* [`Comment`](/guide/api/vue-lynx/Variable.Comment.md) | | `children`? | `string` | `number` | `boolean` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | *typeof* [`Text`](/guide/api/vue-lynx/Variable.Text.md) | *typeof* [`Comment`](/guide/api/vue-lynx/Variable.Comment.md) | | `props`? | `null` | | `children`? | `string` | `number` | `boolean` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | () => `object` | | `type.__isFragment` | `true` | | `children`? | `VNodeArrayChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | () => `object` | | `type.__isFragment` | `true` | | `props`? | `null` | `RawProps` | | `children`? | `VNodeArrayChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props, children): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | () => `object` | | `type.__isTeleport` | `true` | | `props` | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `TeleportProps` | | `children` | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | () => `object` | | `type.__isSuspense` | `true` | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | () => `object` | | `type.__isSuspense` | `true` | | `props`? | `null` | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `SuspenseProps` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `P` | - | | `E` *extends* [`EmitsOptions`](/guide/api/vue-lynx/TypeAlias.EmitsOptions.md) | `object` | | `S` *extends* `Record`\<`string`, `any`> | `any` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | [`FunctionalComponent`](/guide/api/vue-lynx/Interface.FunctionalComponent.md)\<`P`, `any`, `S`, `any`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | `IfAny`\<`S`, `RawSlots`, `S`> | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | [`Component`](/guide/api/vue-lynx/TypeAlias.Component.md)\<`any`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h

(type, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | `ConcreteComponent`\<`object`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | `ConcreteComponent`\<`P`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | [`Component`](/guide/api/vue-lynx/TypeAlias.Component.md)\<`P`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `props`? | `null` | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `ComponentOptions`\<`P`, `any`, `any`, `any`, `any`, `any`, `any`, `any`, `string`, `object`, `object`, `string`, `object`, `object`, `object`, `string`, `ComponentProvideOptions`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `Constructor`\<`any`> | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `Constructor`\<`P`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | [`DefineComponent`](/guide/api/vue-lynx/TypeAlias.DefineComponent.md)\<`object`, `object`, `object`, `ComputedOptions`, `MethodOptions`, `ComponentOptionsMixin`, `ComponentOptionsMixin`, `object`, `string`, `PublicProps`, `Readonly`\<[`ExtractPropTypes`](/guide/api/vue-lynx/TypeAlias.ExtractPropTypes.md)\<`object`>>, `object`, `object`, `object`, `object`, `string`, `ComponentProvideOptions`, `true`, `object`, `any`> | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | [`DefineComponent`](/guide/api/vue-lynx/TypeAlias.DefineComponent.md)\<`P`, `object`, `object`, `ComputedOptions`, `MethodOptions`, `ComponentOptionsMixin`, `ComponentOptionsMixin`, `object`, `string`, `PublicProps`, `Readonly`\<`P` *extends* `ComponentPropsOptions`\<`Data`> ? [`ExtractPropTypes`](/guide/api/vue-lynx/TypeAlias.ExtractPropTypes.md)\<`P`\<`P`>> : `P`>, `ExtractDefaultPropTypes`\<`P`>, `object`, `object`, `object`, `string`, `ComponentProvideOptions`, `true`, `object`, `any`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, children) ```ts function h(type, children?): VNode ``` ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | [`Component`](/guide/api/vue-lynx/TypeAlias.Component.md)\<`any`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `children`? | `RawChildren` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 ## h(type, props, children) ```ts function h

( type, props?, children?): VNode ``` ### Type Parameters | Type Parameter | | ------ | | `P` | ### Parameters | Parameter | Type | | ------ | ------ | | `type` | `string` | [`Component`](/guide/api/vue-lynx/TypeAlias.Component.md)\<`P`, `any`, `any`, `ComputedOptions`, `MethodOptions`, `object`, `any`> | | `props`? | `VNodeProps` & `object` & `Record`\<`string`, `any`> & `P` | `object` *extends* `P` ? `null` : `never` | | `children`? | `RawChildren` | `RawSlots` | ### Returns [`VNode`](/guide/api/vue-lynx/Interface.VNode.md) ### Defined in packages/vue-lynx/runtime/src/index.ts:803 --- url: /guide/api/vue-lynx/Function.hasInjectionContext.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / hasInjectionContext # Function: hasInjectionContext() ```ts function hasInjectionContext(): boolean ``` Returns true if `inject()` can be used without warning about being called in the wrong place (e.g. outside of setup()). This is used by libraries that want to use `inject()` internally without triggering a warning to the end user. One example is `useRoute()` in `vue-router`. ## Returns `boolean` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:980 --- url: /guide/api/vue-lynx/Function.inject.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / inject # Function: inject() ## inject(key) ```ts function inject(key): T | undefined ``` ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | | ------ | ------ | | `key` | `string` | [`InjectionKey`](/guide/api/vue-lynx/TypeAlias.InjectionKey.md)\<`T`> | ### Returns `T` | `undefined` ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:972 ## inject(key, defaultValue, treatDefaultAsFactory) ```ts function inject( key, defaultValue, treatDefaultAsFactory?): T ``` ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | | ------ | ------ | | `key` | `string` | [`InjectionKey`](/guide/api/vue-lynx/TypeAlias.InjectionKey.md)\<`T`> | | `defaultValue` | `T` | | `treatDefaultAsFactory`? | `false` | ### Returns `T` ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:973 ## inject(key, defaultValue, treatDefaultAsFactory) ```ts function inject( key, defaultValue, treatDefaultAsFactory): T ``` ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | | ------ | ------ | | `key` | `string` | [`InjectionKey`](/guide/api/vue-lynx/TypeAlias.InjectionKey.md)\<`T`> | | `defaultValue` | `T` | () => `T` | | `treatDefaultAsFactory` | `true` | ### Returns `T` ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:974 --- url: /guide/api/vue-lynx/Function.isIfrMainThread.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isIfrMainThread # Function: isIfrMainThread() ```ts function isIfrMainThread(): boolean ``` ## Returns `boolean` ## Defined in packages/vue-lynx/runtime/src/ifr-env.ts:28 --- url: /guide/api/vue-lynx/Function.isMemoSame.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isMemoSame # Function: isMemoSame() ```ts function isMemoSame(cached, memo): boolean ``` ## Parameters | Parameter | Type | | ------ | ------ | | `cached` | [`VNode`](/guide/api/vue-lynx/Interface.VNode.md)\<`RendererNode`, `RendererElement`, `object`> | | `memo` | `any`\[] | ## Returns `boolean` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1712 --- url: /guide/api/vue-lynx/Function.isProxy.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isProxy # Function: isProxy() ```ts function isProxy(value): boolean ``` Checks if an object is a proxy created by [reactive](/guide/api/vue-lynx/Function.reactive.md), [readonly](/guide/api/vue-lynx/Function.readonly.md), [shallowReactive](/guide/api/vue-lynx/Function.shallowReactive.md) or [shallowReadonly](/guide/api/vue-lynx/Function.shallowReadonly.md). ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `any` | The value to check. | ## Returns `boolean` ## See [https://vuejs.org/api/reactivity-utilities.html#isproxy](https://vuejs.org/api/reactivity-utilities.html#isproxy) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:185 --- url: /guide/api/vue-lynx/Function.isReactive.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isReactive # Function: isReactive() ```ts function isReactive(value): boolean ``` Checks if an object is a proxy created by [reactive](/guide/api/vue-lynx/Function.reactive.md) or [shallowReactive](/guide/api/vue-lynx/Function.shallowReactive.md) (or [ref](/guide/api/vue-lynx/Function.ref.md) in some cases). ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `unknown` | The value to check. | ## Returns `boolean` ## Example ```js isReactive(reactive({})) // => true isReactive(readonly(reactive({}))) // => true isReactive(ref({}).value) // => true isReactive(readonly(ref({})).value) // => true isReactive(ref(true)) // => false isReactive(shallowRef({}).value) // => false isReactive(shallowReactive({})) // => true ``` ## See [https://vuejs.org/api/reactivity-utilities.html#isreactive](https://vuejs.org/api/reactivity-utilities.html#isreactive) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:164 --- url: /guide/api/vue-lynx/Function.isReadonly.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isReadonly # Function: isReadonly() ```ts function isReadonly(value): boolean ``` Checks whether the passed value is a readonly object. The properties of a readonly object can change, but they can't be assigned directly via the passed object. The proxies created by [readonly](/guide/api/vue-lynx/Function.readonly.md) and [shallowReadonly](/guide/api/vue-lynx/Function.shallowReadonly.md) are both considered readonly, as is a computed ref without a set function. ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `unknown` | The value to check. | ## Returns `boolean` ## See [https://vuejs.org/api/reactivity-utilities.html#isreadonly](https://vuejs.org/api/reactivity-utilities.html#isreadonly) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:176 --- url: /guide/api/vue-lynx/Function.isRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isRef # Function: isRef() ```ts function isRef(r): r is Ref ``` Checks if a value is a ref object. ## Type Parameters | Type Parameter | | ------ | | `T` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `r` | `unknown` | The value to inspect. | ## Returns `r is Ref` ## See [https://vuejs.org/api/reactivity-utilities.html#isref](https://vuejs.org/api/reactivity-utilities.html#isref) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:432 --- url: /guide/api/vue-lynx/Function.isShallow.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isShallow # Function: isShallow() ```ts function isShallow(value): boolean ``` ## Parameters | Parameter | Type | | ------ | ------ | | `value` | `unknown` | ## Returns `boolean` ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:177 --- url: /guide/api/vue-lynx/Function.isVNode.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / isVNode # Function: isVNode() ```ts function isVNode(value): value is VNode ``` ## Parameters | Parameter | Type | | ------ | ------ | | `value` | `any` | ## Returns `value is VNode` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1280 --- url: /guide/api/vue-lynx/Function.markRaw.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / markRaw # Function: markRaw() ```ts function markRaw(value): Raw ``` Marks an object so that it will never be converted to a proxy. Returns the object itself. ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `T` | The object to be marked as "raw". | ## Returns `Raw`\<`T`> ## Example ```js const foo = markRaw({}) console.log(isReactive(reactive(foo))) // false // also works when nested inside other reactive objects const bar = reactive({ foo }) console.log(isReactive(bar.foo)) // false ``` **Warning:** `markRaw()` together with the shallow APIs such as [shallowReactive](/guide/api/vue-lynx/Function.shallowReactive.md) allow you to selectively opt-out of the default deep reactive/readonly conversion and embed raw, non-proxied objects in your state graph. ## See [https://vuejs.org/api/reactivity-advanced.html#markraw](https://vuejs.org/api/reactivity-advanced.html#markraw) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:235 --- url: /guide/api/vue-lynx/Function.mergeProps.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / mergeProps # Function: mergeProps() ```ts function mergeProps(...args): Data ``` ## Parameters | Parameter | Type | | ------ | ------ | | ...`args` | `Data` & `VNodeProps`\[] | ## Returns `Data` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1307 --- url: /guide/api/vue-lynx/Function.nextTick.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / nextTick # Function: nextTick() ```ts function nextTick(fn?): Promise ``` Wait for the next DOM update flush **and** the main-thread ops acknowledgement. Unlike standard Vue's `nextTick` which only waits for the scheduler flush, Vue Lynx's version also waits for the main thread to apply the ops, so native Lynx elements are fully materialised when the callback fires. Caveat: some Lynx builds never invoke the `callLepusMethod` callback that carries the acknowledgement. Until the engine has delivered one real acknowledgement, each flush falls back to a short timer so `nextTick()` cannot hang forever — on such engines the materialisation guarantee is best-effort (a dev-mode warning is logged when the fallback fires). Once a real acknowledgement has been observed, the strict guarantee applies. ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `fn`? | () => `void` | Optional callback to execute after flush | ## Returns `Promise`\<`void`> A promise that resolves when the main thread has applied all pending ops ## See [Vue nextTick](https://vuejs.org/api/general.html#nexttick) — Vue Lynx extends the standard behavior. ## Defined in packages/vue-lynx/runtime/src/index.ts:250 --- url: /guide/api/vue-lynx/Function.normalizeClass.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / normalizeClass # Function: normalizeClass() ```ts function normalizeClass(value): string ``` ## Parameters | Parameter | Type | | ------ | ------ | | `value` | `unknown` | ## Returns `string` ## Defined in node\_modules/.pnpm/@vue+shared@3.5.30/node\_modules/@vue/shared/dist/shared.d.ts:240 --- url: /guide/api/vue-lynx/Function.normalizeProps.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / normalizeProps # Function: normalizeProps() ```ts function normalizeProps(props): Record | null ``` ## Parameters | Parameter | Type | | ------ | ------ | | `props` | `null` | `Record`\<`string`, `any`> | ## Returns `Record`\<`string`, `any`> | `null` ## Defined in node\_modules/.pnpm/@vue+shared@3.5.30/node\_modules/@vue/shared/dist/shared.d.ts:241 --- url: /guide/api/vue-lynx/Function.normalizeStyle.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / normalizeStyle # Function: normalizeStyle() ```ts function normalizeStyle(value): NormalizedStyle | string | undefined ``` ## Parameters | Parameter | Type | | ------ | ------ | | `value` | `unknown` | ## Returns `NormalizedStyle` | `string` | `undefined` ## Defined in node\_modules/.pnpm/@vue+shared@3.5.30/node\_modules/@vue/shared/dist/shared.d.ts:237 --- url: /guide/api/vue-lynx/Function.onActivated.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onActivated # Function: onActivated() ```ts function onActivated(hook, target?): void ``` Registers a hook to be called when the component is inserted into the DOM as part of a tree cached by ``. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `Function` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onactivated) ## Defined in packages/vue-lynx/runtime/src/index.ts:607 --- url: /guide/api/vue-lynx/Function.onBeforeMount.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onBeforeMount # Function: onBeforeMount() ```ts function onBeforeMount(hook, target?): void ``` Registers a hook to be called right before the component is to be mounted. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onbeforemount) ## Defined in packages/vue-lynx/runtime/src/index.ts:535 --- url: /guide/api/vue-lynx/Function.onBeforeUnmount.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onBeforeUnmount # Function: onBeforeUnmount() ```ts function onBeforeUnmount(hook, target?): void ``` Registers a hook to be called right before the component is about to be unmounted. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onbeforeunmount) ## Defined in packages/vue-lynx/runtime/src/index.ts:551 --- url: /guide/api/vue-lynx/Function.onBeforeUpdate.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onBeforeUpdate # Function: onBeforeUpdate() ```ts function onBeforeUpdate(hook, target?): void ``` Registers a hook to be called right before the component is about to update. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onbeforeupdate) ## Defined in packages/vue-lynx/runtime/src/index.ts:569 --- url: /guide/api/vue-lynx/Function.onDeactivated.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onDeactivated # Function: onDeactivated() ```ts function onDeactivated(hook, target?): void ``` Registers a hook to be called when the component is removed from the DOM as part of a tree cached by ``. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `Function` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#ondeactivated) ## Defined in packages/vue-lynx/runtime/src/index.ts:616 --- url: /guide/api/vue-lynx/Function.onErrorCaptured.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onErrorCaptured # Function: onErrorCaptured() ```ts function onErrorCaptured(hook, target?): void ``` ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `TError` | `Error` | ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `ErrorCapturedHook`\<`TError`> | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:739 --- url: /guide/api/vue-lynx/Function.onMounted.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onMounted # Function: onMounted() ```ts function onMounted(hook, target?): void ``` Registers a callback to be called after the component is mounted. In Vue Lynx, the callback only runs on the background thread — during an IFR main-thread first-screen render, registration is a no-op. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onmounted) ## Defined in packages/vue-lynx/runtime/src/index.ts:527 --- url: /guide/api/vue-lynx/Function.onRenderTracked.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onRenderTracked # Function: onRenderTracked() ```ts function onRenderTracked(hook, target?): void ``` ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `DebuggerHook` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:737 --- url: /guide/api/vue-lynx/Function.onRenderTriggered.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onRenderTriggered # Function: onRenderTriggered() ```ts function onRenderTriggered(hook, target?): void ``` ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `DebuggerHook` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:736 --- url: /guide/api/vue-lynx/Function.onScopeDispose.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onScopeDispose # Function: onScopeDispose() ```ts function onScopeDispose(fn, failSilently?): void ``` Registers a dispose callback on the current active effect scope. The callback will be invoked when the associated effect scope is stopped. ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `fn` | () => `void` | The callback function to attach to the scope's cleanup. | | `failSilently`? | `boolean` | - | ## Returns `void` ## See [https://vuejs.org/api/reactivity-advanced.html#onscopedispose](https://vuejs.org/api/reactivity-advanced.html#onscopedispose) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:699 --- url: /guide/api/vue-lynx/Function.onUnmounted.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onUnmounted # Function: onUnmounted() ```ts function onUnmounted(hook, target?): void ``` Registers a callback to be called after the component is unmounted. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onunmounted) ## Defined in packages/vue-lynx/runtime/src/index.ts:543 --- url: /guide/api/vue-lynx/Function.onUpdated.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onUpdated # Function: onUpdated() ```ts function onUpdated(hook, target?): void ``` Registers a callback to be called after the component has updated its DOM tree. ## Parameters | Parameter | Type | | ------ | ------ | | `hook` | `any` | | `target`? | `null` | [`ComponentInternalInstance`](/guide/api/vue-lynx/Interface.ComponentInternalInstance.md) | ## Returns `void` ## See [Vue docs](https://vuejs.org/api/composition-api-lifecycle.html#onupdated) ## Defined in packages/vue-lynx/runtime/src/index.ts:561 --- url: /guide/api/vue-lynx/Function.onWatcherCleanup.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / onWatcherCleanup # Function: onWatcherCleanup() ```ts function onWatcherCleanup( cleanupFn, failSilently?, owner?): void ``` Registers a cleanup callback on the current active effect. This registered cleanup callback will be invoked right before the associated effect re-runs. ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `cleanupFn` | () => `void` | The callback function to attach to the effect's cleanup. | | `failSilently`? | `boolean` | if `true`, will not throw warning when called without an active effect. | | `owner`? | `ReactiveEffect`\<`any`> | The effect that this cleanup function should be attached to. By default, the current active effect. | ## Returns `void` ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:750 --- url: /guide/api/vue-lynx/Function.provide.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / provide # Function: provide() ```ts function provide(key, value): void ``` ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` | - | | `K` | `string` | `number` | [`InjectionKey`](/guide/api/vue-lynx/TypeAlias.InjectionKey.md)\<`T`> | ## Parameters | Parameter | Type | | ------ | ------ | | `key` | `K` | | `value` | `K` *extends* [`InjectionKey`](/guide/api/vue-lynx/TypeAlias.InjectionKey.md)\<`V`> ? `V` : `T` | ## Returns `void` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:971 --- url: /guide/api/vue-lynx/Function.reactive.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / reactive # Function: reactive() ```ts function reactive(target): Reactive ``` Returns a reactive proxy of the object. The reactive conversion is "deep": it affects all nested properties. A reactive object also deeply unwraps any properties that are refs while maintaining reactivity. ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `target` | `T` | The source object. | ## Returns [`Reactive`](/guide/api/vue-lynx/TypeAlias.Reactive.md)\<`T`> ## Example ```js const obj = reactive({ count: 0 }) ``` ## See [https://vuejs.org/api/reactivity-core.html#reactive](https://vuejs.org/api/reactivity-core.html#reactive) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:44 --- url: /guide/api/vue-lynx/Function.readonly.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / readonly # Function: readonly() ```ts function readonly(target): DeepReadonly> ``` Takes an object (reactive or plain) or a ref and returns a readonly proxy to the original. A readonly proxy is deep: any nested property accessed will be readonly as well. It also has the same ref-unwrapping behavior as [reactive](/guide/api/vue-lynx/Function.reactive.md), except the unwrapped values will also be made readonly. ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `target` | `T` | The source object. | ## Returns [`DeepReadonly`](/guide/api/vue-lynx/TypeAlias.DeepReadonly.md)\<[`UnwrapNestedRefs`](/guide/api/vue-lynx/TypeAlias.UnwrapNestedRefs.md)\<`T`>> ## Example ```js const original = reactive({ count: 0 }) const copy = readonly(original) watchEffect(() => { // works for reactivity tracking console.log(copy.count) }) // mutating original will trigger watchers relying on the copy original.count++ // mutating the copy will fail and result in a warning copy.count++ // warning! ``` ## See [https://vuejs.org/api/reactivity-core.html#readonly](https://vuejs.org/api/reactivity-core.html#readonly) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:114 --- url: /guide/api/vue-lynx/Function.ref.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / ref # Function: ref() ## ref(value) ```ts function ref(value): [T] extends [Ref] ? IfAny, T> : Ref, UnwrapRef | T> ``` Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value. ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `T` | The object to wrap in the ref. | ### Returns \[`T`] *extends* \[[`Ref`](/guide/api/vue-lynx/Interface.Ref.md)] ? `IfAny`\<`T`, [`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<`T`>, `T`> : [`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<[`UnwrapRef`](/guide/api/vue-lynx/TypeAlias.UnwrapRef.md)\<`T`>, [`UnwrapRef`](/guide/api/vue-lynx/TypeAlias.UnwrapRef.md)\<`T`> | `T`> ### See [https://vuejs.org/api/reactivity-core.html#ref](https://vuejs.org/api/reactivity-core.html#ref) ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:440 ## ref() ```ts function ref(): Ref ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` | `any` | ### Returns [`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<`T` | `undefined`> ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:441 --- url: /guide/api/vue-lynx/Function.runOnBackground.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / runOnBackground # Function: runOnBackground() ```ts function runOnBackground(_fn): (...args) => Promise ``` Call a Background Thread function from a `'main thread'` worklet. At build time the SWC transform replaces all `runOnBackground(fn)` call sites. This export exists only for TypeScript import resolution — it is never called at runtime on the BG thread. ## Type Parameters | Type Parameter | | ------ | | `R` | | `Fn` *extends* (...`args`) => `R` | ## Parameters | Parameter | Type | | ------ | ------ | | `_fn` | `Fn` | ## Returns `Function` ### Parameters | Parameter | Type | | ------ | ------ | | ...`args` | `Parameters`\<`Fn`> | ### Returns `Promise`\<`R`> ## Defined in packages/vue-lynx/runtime/src/run-on-background.ts:141 --- url: /guide/api/vue-lynx/Function.runOnMainThread.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / runOnMainThread # Function: runOnMainThread() ```ts function runOnMainThread(fn): (...args) => Promise ``` Mark a function to be executed on the Main Thread. Returns a wrapper that, when called from the Background Thread, dispatches the call to the Main Thread via the worklet runtime and returns a Promise that resolves to the function's return value. The SWC worklet transform replaces the `fn` argument with a worklet context object at build time. Without the transform, `fn` is passed through as-is (dev warning in non-production builds). ## Type Parameters | Type Parameter | | ------ | | `R` | | `Fn` *extends* (...`args`) => `R` | ## Parameters | Parameter | Type | | ------ | ------ | | `fn` | `Fn` | ## Returns `Function` ### Parameters | Parameter | Type | | ------ | ------ | | ...`args` | `Parameters`\<`Fn`> | ### Returns `Promise`\<`R`> ## Example ```ts const animate = runOnMainThread((x: number) => { 'main thread' element.setStyleProperty('opacity', String(x)) }) await animate(0.5) // executes on Main Thread ``` ## Defined in packages/vue-lynx/runtime/src/cross-thread.ts:41 --- url: /guide/api/vue-lynx/Function.shallowReactive.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / shallowReactive # Function: shallowReactive() ```ts function shallowReactive(target): ShallowReactive ``` Shallow version of [reactive](/guide/api/vue-lynx/Function.reactive.md). Unlike [reactive](/guide/api/vue-lynx/Function.reactive.md), there is no deep conversion: only root-level properties are reactive for a shallow reactive object. Property values are stored and exposed as-is - this also means properties with ref values will not be automatically unwrapped. ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `target` | `T` | The source object. | ## Returns `ShallowReactive`\<`T`> ## Example ```js const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) // mutating state's own properties is reactive state.foo++ // ...but does not convert nested objects isReactive(state.nested) // false // NOT reactive state.nested.bar++ ``` ## See [https://vuejs.org/api/reactivity-advanced.html#shallowreactive](https://vuejs.org/api/reactivity-advanced.html#shallowreactive) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:79 --- url: /guide/api/vue-lynx/Function.shallowReadonly.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / shallowReadonly # Function: shallowReadonly() ```ts function shallowReadonly(target): Readonly ``` Shallow version of [readonly](/guide/api/vue-lynx/Function.readonly.md). Unlike [readonly](/guide/api/vue-lynx/Function.readonly.md), there is no deep conversion: only root-level properties are made readonly. Property values are stored and exposed as-is - this also means properties with ref values will not be automatically unwrapped. ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `target` | `T` | The source object. | ## Returns `Readonly`\<`T`> ## Example ```js const state = shallowReadonly({ foo: 1, nested: { bar: 2 } }) // mutating state's own properties will fail state.foo++ // ...but works on nested objects isReadonly(state.nested) // false // works state.nested.bar++ ``` ## See [https://vuejs.org/api/reactivity-advanced.html#shallowreadonly](https://vuejs.org/api/reactivity-advanced.html#shallowreadonly) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:145 --- url: /guide/api/vue-lynx/Function.shallowRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / shallowRef # Function: shallowRef() ## shallowRef(value) ```ts function shallowRef(value): Ref extends T ? T extends Ref ? IfAny, T> : ShallowRef : ShallowRef ``` Shallow version of [ref](/guide/api/vue-lynx/Function.ref.md). ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `value` | `T` | The "inner value" for the shallow ref. | ### Returns [`Ref`](/guide/api/vue-lynx/Interface.Ref.md) *extends* `T` ? `T` *extends* [`Ref`](/guide/api/vue-lynx/Interface.Ref.md) ? `IfAny`\<`T`, [`ShallowRef`](/guide/api/vue-lynx/TypeAlias.ShallowRef.md)\<`T`>, `T`> : [`ShallowRef`](/guide/api/vue-lynx/TypeAlias.ShallowRef.md)\<`T`> : [`ShallowRef`](/guide/api/vue-lynx/TypeAlias.ShallowRef.md)\<`T`> ### Example ```js const state = shallowRef({ count: 1 }) // does NOT trigger change state.value.count = 2 // does trigger change state.value = { count: 2 } ``` ### See [https://vuejs.org/api/reactivity-advanced.html#shallowref](https://vuejs.org/api/reactivity-advanced.html#shallowref) ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:463 ## shallowRef() ```ts function shallowRef(): ShallowRef ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` | `any` | ### Returns [`ShallowRef`](/guide/api/vue-lynx/TypeAlias.ShallowRef.md)\<`T` | `undefined`> ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:464 --- url: /guide/api/vue-lynx/Function.takeOps.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / takeOps # Function: takeOps() ```ts function takeOps(): unknown[] ``` ## Returns `unknown`\[] ## Defined in packages/vue-lynx/runtime/src/ops.ts:15 --- url: /guide/api/vue-lynx/Function.toRaw.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / toRaw # Function: toRaw() ```ts function toRaw(observed): T ``` Returns the raw, original object of a Vue-created proxy. `toRaw()` can return the original object from proxies created by [reactive](/guide/api/vue-lynx/Function.reactive.md), [readonly](/guide/api/vue-lynx/Function.readonly.md), [shallowReactive](/guide/api/vue-lynx/Function.shallowReactive.md) or [shallowReadonly](/guide/api/vue-lynx/Function.shallowReadonly.md). This is an escape hatch that can be used to temporarily read without incurring proxy access / tracking overhead or write without triggering changes. It is **not** recommended to hold a persistent reference to the original object. Use with caution. ## Type Parameters | Type Parameter | | ------ | | `T` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `observed` | `T` | The object for which the "raw" value is requested. | ## Returns `T` ## Example ```js const foo = {} const reactiveFoo = reactive(foo) console.log(toRaw(reactiveFoo) === foo) // true ``` ## See [https://vuejs.org/api/reactivity-advanced.html#toraw](https://vuejs.org/api/reactivity-advanced.html#toraw) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:209 --- url: /guide/api/vue-lynx/Function.toRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / toRef # Function: toRef() ## toRef(value) ```ts function toRef(value): T extends () => infer R ? Readonly> : T extends Ref ? T : Ref> ``` Used to normalize values / refs / getters into refs. ### Type Parameters | Type Parameter | | ------ | | `T` | ### Parameters | Parameter | Type | | ------ | ------ | | `value` | `T` | ### Returns `T` *extends* () => infer R ? `Readonly`\<[`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<`R`>> : `T` *extends* [`Ref`](/guide/api/vue-lynx/Interface.Ref.md) ? `T` : [`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<[`UnwrapRef`](/guide/api/vue-lynx/TypeAlias.UnwrapRef.md)\<`T`>> ### Examples ```js // returns existing refs as-is toRef(existingRef) // creates a ref that calls the getter on .value access toRef(() => props.foo) // creates normal refs from non-function values // equivalent to ref(1) toRef(1) ``` Can also be used to create a ref for a property on a source reactive object. The created ref is synced with its source property: mutating the source property will update the ref, and vice-versa. ```js const state = reactive({ foo: 1, bar: 2 }) const fooRef = toRef(state, 'foo') // mutating the ref updates the original fooRef.value++ console.log(state.foo) // 2 // mutating the original also updates the ref state.foo++ console.log(fooRef.value) // 3 ``` ### See [https://vuejs.org/api/reactivity-utilities.html#toref](https://vuejs.org/api/reactivity-utilities.html#toref) ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:604 ## toRef(object, key) ```ts function toRef(object, key): ToRef ``` ### Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | | `K` *extends* `string` | `number` | `symbol` | ### Parameters | Parameter | Type | | ------ | ------ | | `object` | `T` | | `key` | `K` | ### Returns `ToRef`\<`T`\[`K`]> ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:605 ## toRef(object, key, defaultValue) ```ts function toRef( object, key, defaultValue): ToRef> ``` ### Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | | `K` *extends* `string` | `number` | `symbol` | ### Parameters | Parameter | Type | | ------ | ------ | | `object` | `T` | | `key` | `K` | | `defaultValue` | `T`\[`K`] | ### Returns `ToRef`\<`Exclude`\<`T`\[`K`], `undefined`>> ### Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:606 --- url: /guide/api/vue-lynx/Function.toRefs.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / toRefs # Function: toRefs() ```ts function toRefs(object): ToRefs ``` Converts a reactive object to a plain object where each property of the resulting object is a ref pointing to the corresponding property of the original object. Each individual ref is created using [toRef](/guide/api/vue-lynx/Function.toRef.md). ## Type Parameters | Type Parameter | | ------ | | `T` *extends* `object` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `object` | `T` | Reactive object to be made into an object of linked refs. | ## Returns `ToRefs`\<`T`> ## See [https://vuejs.org/api/reactivity-utilities.html#torefs](https://vuejs.org/api/reactivity-utilities.html#torefs) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:559 --- url: /guide/api/vue-lynx/Function.toValue.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / toValue # Function: toValue() ```ts function toValue(source): T ``` Normalizes values / refs / getters to values. This is similar to [unref](/guide/api/vue-lynx/Function.unref.md), except that it also normalizes getters. If the argument is a getter, it will be invoked and its return value will be returned. ## Type Parameters | Type Parameter | | ------ | | `T` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `source` | [`MaybeRefOrGetter`](/guide/api/vue-lynx/TypeAlias.MaybeRefOrGetter.md)\<`T`> | A getter, an existing ref, or a non-function value. | ## Returns `T` ## Example ```js toValue(1) // 1 toValue(ref(1)) // 1 toValue(() => 1) // 1 ``` ## See [https://vuejs.org/api/reactivity-utilities.html#tovalue](https://vuejs.org/api/reactivity-utilities.html#tovalue) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:526 --- url: /guide/api/vue-lynx/Function.transformToWorklet.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / transformToWorklet # Function: transformToWorklet() ```ts function transformToWorklet(obj): JsFnHandle ``` ## Parameters | Parameter | Type | | ------ | ------ | | `obj` | (...`args`) => `unknown` | ## Returns `JsFnHandle` ## Defined in packages/vue-lynx/runtime/src/transform-to-worklet.ts:17 --- url: /guide/api/vue-lynx/Function.triggerRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / triggerRef # Function: triggerRef() ```ts function triggerRef(ref): void ``` Force trigger effects that depends on a shallow ref. This is typically used after making deep mutations to the inner value of a shallow ref. ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `ref` | [`Ref`](/guide/api/vue-lynx/Interface.Ref.md)\<`any`, `any`> | The ref whose tied effects shall be executed. | ## Returns `void` ## Example ```js const shallow = shallowRef({ greet: 'Hello, world' }) // Logs "Hello, world" once for the first run-through watchEffect(() => { console.log(shallow.value.greet) }) // This won't trigger the effect because the ref is shallow shallow.value.greet = 'Hello, universe' // Logs "Hello, universe" triggerRef(shallow) ``` ## See [https://vuejs.org/api/reactivity-advanced.html#triggerref](https://vuejs.org/api/reactivity-advanced.html#triggerref) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:490 --- url: /guide/api/vue-lynx/Function.unref.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / unref # Function: unref() ```ts function unref(ref): T ``` Returns the inner value if the argument is a ref, otherwise return the argument itself. This is a sugar function for `val = isRef(val) ? val.value : val`. ## Type Parameters | Type Parameter | | ------ | | `T` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `ref` | [`MaybeRef`](/guide/api/vue-lynx/TypeAlias.MaybeRef.md)\<`T`> | [`ComputedRef`](/guide/api/vue-lynx/Interface.ComputedRef.md)\<`T`> | Ref or plain value to be converted into the plain value. | ## Returns `T` ## Example ```js function useFoo(x: number | Ref) { const unwrapped = unref(x) // unwrapped is guaranteed to be number now } ``` ## See [https://vuejs.org/api/reactivity-utilities.html#unref](https://vuejs.org/api/reactivity-utilities.html#unref) ## Defined in node\_modules/.pnpm/@vue+reactivity@3.5.30/node\_modules/@vue/reactivity/dist/reactivity.d.ts:509 --- url: /guide/api/vue-lynx/Function.useAttrs.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useAttrs # Function: useAttrs() ```ts function useAttrs(): SetupContext["attrs"] ``` ## Returns [`SetupContext`](/guide/api/vue-lynx/TypeAlias.SetupContext.md)\[`"attrs"`] ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:364 --- url: /guide/api/vue-lynx/Function.useGlobalEvent.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useGlobalEvent # Function: useGlobalEvent() ```ts function useGlobalEvent(eventName, handler): void ``` Registers a listener on Lynx's global event emitter for the current scope. ## Parameters | Parameter | Type | | ------ | ------ | | `eventName` | `string` | | `handler` | `GlobalEventHandler` | ## Returns `void` ## Defined in packages/vue-lynx/runtime/src/use-global-event.ts:14 --- url: /guide/api/vue-lynx/Function.useId.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useId # Function: useId() ```ts function useId(): string ``` ## Returns `string` ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1542 --- url: /guide/api/vue-lynx/Function.useMainThreadRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useMainThreadRef # Function: useMainThreadRef() ```ts function useMainThreadRef(initValue): MainThreadRef ``` Create a MainThreadRef — a ref whose `.value` is reactive (read-only) on the Background Thread and whose `.current` is read-write on the Main Thread inside worklet functions. ## Type Parameters | Type Parameter | | ------ | | `T` | ## Parameters | Parameter | Type | Description | | ------ | ------ | ------ | | `initValue` | `T` | Initial value (typically `null` for element refs, or a primitive for shared state). | ## Returns [`MainThreadRef`](/guide/api/vue-lynx/Class.MainThreadRef.md)\<`T`> ## Example ```ts const elRef = useMainThreadRef(null) // ``` ## Defined in packages/vue-lynx/runtime/src/main-thread-ref.ts:113 --- url: /guide/api/vue-lynx/Function.useModel.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useModel # Function: useModel() ```ts function useModel( props, name, options?): ModelRef ``` ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `M` *extends* `PropertyKey` | - | | `T` *extends* `Record`\<`string`, `any`> | - | | `K` *extends* `string` | `number` | `symbol` | - | | `G` | `T`\[`K`] | | `S` | `T`\[`K`] | ## Parameters | Parameter | Type | | ------ | ------ | | `props` | `T` | | `name` | `K` | | `options`? | `DefineModelOptions`\<`T`\[`K`], `G`, `S`> | ## Returns `ModelRef`\<`T`\[`K`], `M`, `G`, `S`> ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1537 --- url: /guide/api/vue-lynx/Function.useSlots.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useSlots # Function: useSlots() ```ts function useSlots(): SetupContext["slots"] ``` ## Returns [`SetupContext`](/guide/api/vue-lynx/TypeAlias.SetupContext.md)\[`"slots"`] ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:363 --- url: /guide/api/vue-lynx/Function.useTemplateRef.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / useTemplateRef # Function: useTemplateRef() ```ts function useTemplateRef(key): TemplateRef ``` ## Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` | `unknown` | | `Keys` *extends* `string` | `string` | ## Parameters | Parameter | Type | | ------ | ------ | | `key` | `Keys` | ## Returns `TemplateRef`\<`T`> ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1540 --- url: /guide/api/vue-lynx/Function.watch.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / watch # Function: watch() ## watch(source, cb, options) ```ts function watch( source, cb, options?): WatchHandle ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` | - | | `Immediate` *extends* `Readonly`\<`boolean`> | `false` | ### Parameters | Parameter | Type | | ------ | ------ | | `source` | `WatchSource`\<`T`> | | `cb` | `WatchCallback`\<`T`, `MaybeUndefined`\<`T`, `Immediate`>> | | `options`? | [`WatchOptions`](/guide/api/vue-lynx/Interface.WatchOptions.md)\<`Immediate`> | ### Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1498 ## watch(sources, cb, options) ```ts function watch( sources, cb, options?): WatchHandle ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` *extends* readonly (`object` | `WatchSource`\<`unknown`>)\[] | - | | `Immediate` *extends* `Readonly`\<`boolean`> | `false` | ### Parameters | Parameter | Type | | ------ | ------ | | `sources` | `T` | readonly \[`T`] | | `cb` | \[`T`] *extends* \[`ReactiveMarker`] ? `WatchCallback`\<`T`\<`T`>, `MaybeUndefined`\<`T`\<`T`>, `Immediate`>> : `WatchCallback`\<`MapSources`\<`T`, `false`>, `MapSources`\<`T`, `Immediate`>> | | `options`? | [`WatchOptions`](/guide/api/vue-lynx/Interface.WatchOptions.md)\<`Immediate`> | ### Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1499 ## watch(sources, cb, options) ```ts function watch( sources, cb, options?): WatchHandle ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` *extends* `MultiWatchSources` | - | | `Immediate` *extends* `Readonly`\<`boolean`> | `false` | ### Parameters | Parameter | Type | | ------ | ------ | | `sources` | \[`...T[]`] | | `cb` | `WatchCallback`\<`MapSources`\<`T`, `false`>, `MapSources`\<`T`, `Immediate`>> | | `options`? | [`WatchOptions`](/guide/api/vue-lynx/Interface.WatchOptions.md)\<`Immediate`> | ### Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1500 ## watch(source, cb, options) ```ts function watch( source, cb, options?): WatchHandle ``` ### Type Parameters | Type Parameter | Default type | | ------ | ------ | | `T` *extends* `object` | - | | `Immediate` *extends* `Readonly`\<`boolean`> | `false` | ### Parameters | Parameter | Type | | ------ | ------ | | `source` | `T` | | `cb` | `WatchCallback`\<`T`, `MaybeUndefined`\<`T`, `Immediate`>> | | `options`? | [`WatchOptions`](/guide/api/vue-lynx/Interface.WatchOptions.md)\<`Immediate`> | ### Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ### Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1501 --- url: /guide/api/vue-lynx/Function.watchEffect.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / watchEffect # Function: watchEffect() ```ts function watchEffect(effect, options?): WatchHandle ``` ## Parameters | Parameter | Type | | ------ | ------ | | `effect` | `WatchEffect` | | `options`? | `WatchEffectOptions` | ## Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1494 --- url: /guide/api/vue-lynx/Function.watchPostEffect.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / watchPostEffect # Function: watchPostEffect() ```ts function watchPostEffect(effect, options?): WatchHandle ``` ## Parameters | Parameter | Type | | ------ | ------ | | `effect` | `WatchEffect` | | `options`? | `DebuggerOptions` | ## Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1495 --- url: /guide/api/vue-lynx/Function.watchSyncEffect.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / watchSyncEffect # Function: watchSyncEffect() ```ts function watchSyncEffect(effect, options?): WatchHandle ``` ## Parameters | Parameter | Type | | ------ | ------ | | `effect` | `WatchEffect` | | `options`? | `DebuggerOptions` | ## Returns [`WatchHandle`](/guide/api/vue-lynx/Interface.WatchHandle.md) ## Defined in node\_modules/.pnpm/@vue+runtime-core@3.5.30/node\_modules/@vue/runtime-core/dist/runtime-core.d.ts:1496 --- url: /guide/api/vue-lynx/Function.withDefaults.md --- [vue-lynx](/guide/api/vue-lynx/index.md) / withDefaults # Function: withDefaults() ```ts function withDefaults(props, defaults): PropsWithDefaults ``` Vue ` ``` ### Reactive Query Keys A key advantage of Vue Query over its React counterpart is that query keys can be **reactive**. When a `ref` or `computed` value in the key changes, the query automatically refetches — no manual dependency tracking needed. The example app uses this for search filtering and dependent queries: ```vue title="src/App.vue" ``` Here `queryKey` and `enabled` are both `computed` refs. Vue Query watches them reactively — when the user taps a different user, `selectedUserId` changes, the key updates, and the posts for the new user are fetched automatically. No watchers or `onMounted` callbacks needed. ### Mutations with Optimistic Updates Use [`useMutation`](https://tanstack.com/query/v5/docs/framework/vue/reference/useMutation) to modify server data. Optimistic updates let you immediately reflect changes in the UI, rolling back automatically if the request fails: ```vue title="src/App.vue" ``` ## Using `fetch()` Directly For simple one-off requests, you can use the Fetch API directly with Vue's reactivity: ```vue title="src/App.vue" ``` For anything beyond simple reads — caching, background refresh, pagination, mutations — use TanStack Query instead. See [TanStack Query — Vue Overview](https://tanstack.com/query/v5/docs/framework/vue/overview) for additional resources. --- url: /guide/element-templates.md --- # Element Templates Element Templates are now documented together with Instant First-Frame Rendering. See [IFR: Element Templates](/guide/ifr.md#element-templates) for the compiler lowering model, eligibility rules, semantics, configuration, and benchmarks. --- url: /guide/elk.md --- import { Badge } from '@theme'; # Elk — a Mastodon Client To prove Vue Lynx can carry a real product-grade app, we ported [Elk](https://github.com/elk-zone/elk) — the beloved Mastodon web client by Anthony Fu and team — into a **native Mastodon client**. It browses any public Mastodon instance as a guest (default `mas.to`): timelines, threads, profiles, search, trends, dark mode and more. Try it below. The **Web** tab runs the real app on Lynx for Web against a live instance; the **QR Code** tab runs the same `main.lynx.bundle` natively — scan it with Lynx Go / Lynx Explorer (see [Quick Start](/guide/quick-start.md) to install one). ### Native viewpager variant The `elk-viewpager` fork backs the Explore and Notifications tabs with the native [``](https://lynxjs.org/guide/ui/elements-components.html#xelement) element instead of conditionally rendered panes. In [`TabPager.vue`](https://github.com/huxpro/vue-lynx/blob/main/examples/elk-viewpager/src/components/TabPager.vue) each pane is a ``: panes swipe horizontally with a native snap animation, keep content and scroll position across swipes, and the tab bar syncs both ways — a swipe fires the pager's `change` event (→ active tab), a tab tap calls its `selectTab` method (→ animate to that page). It feels much closer to a native client. `` is a Lynx [XElement](https://lynxjs.org/guide/ui/elements-components.html#xelement), registered under a different tag per platform, so `TabPager.vue` picks the tag at runtime from `SystemInfo.platform` (highlighted below): `` on Lynx for Web, the extracted `` / `` on native OSS engines. :::warning Engine version dependency The extracted `` landed in the OSS engine in lynx-family/lynx `c1d8d7920` (2026-04) — **newer than any released LynxExplorer**. 3.8.1 and earlier register neither `` nor ``, so the tab area renders blank (with a `LynxCreateUIException`). Run this variant on a host built from lynx `develop`; on released Explorers, use the default Elk example above, whose conditional tabs need no pager element. ::: #### How much code does the native upgrade take? Almost none. Moving the whole app onto the native pager touches **three files**: the new `TabPager.vue` plus the two pages with swipeable tabs (`ExplorePage.vue`, `NotificationsPage.vue`). The other ~55 source files — masto.js client, content renderer, router, virtualized `` — are byte-for-byte identical. At the call site it's a straight swap. The hand-rolled tab bar and `v-if` / `v-else-if` panes, where only the active pane exists: ```vue ``` …become a component with one named slot per pane: ```vue ``` That `v-if` → slot rewrite is the whole UI-side story — tab bar, underline animation, and pager ↔ tab sync all move into `TabPager`. One change isn't mechanical, and it captures what "native paging" really means. With `v-if`, only the visible pane exists, so a single shared paginator was enough. With the pager, **every pane is mounted at once** and swipeable — so each tab keeps its own data and scroll position. Notifications goes from one shared feed to one per tab: ```diff - let pager = signedIn ? makePaginator() : empty; - const items = ref([]); // one active feed, reset on tab switch + const feeds = reactive({ // one feed per pane, retained across swipes + all: { items: [], state: 'idle' }, + mention: { items: [], state: 'idle' }, + }); ``` That's the trade: you give up "render only what's visible," and the panes stay warm — swipe away and back, and you're exactly where you left off, no reload. #### Going further: a collapsing profile The profile page takes the pattern one step further into "native profile" territory. [`AccountPage`](https://github.com/huxpro/vue-lynx/blob/main/examples/elk-viewpager/src/pages/AccountPage.vue) wraps the same viewpager in Lynx's collapsing-header coordinator ([`StickyTabView.vue`](https://github.com/huxpro/vue-lynx/blob/main/examples/elk-viewpager/src/components/StickyTabView.vue)): scroll down and the banner/bio/stats **header collapses**, the **tab bar pins** to the top, and the Posts / Replies / Media panes below keep **paging horizontally**, each with its own feed and scroll position. The coordinator stacks as header (collapses) + toolbar (sticky tabs) + slot (the viewpager), and its nested scroll folds the header first, then hands off to the active pane's list. Like the viewpager it's registered per platform — the legacy `` on Lynx for Web, the extracted [``](https://lynxjs.org/guide/ui/elements-components.html#xelement) on native OSS engines. ## Native feed techniques A Mastodon client is mostly long lists: home, local, federated, explore, notifications, search, profile posts, followers. On the web Elk virtualizes with [`virtua`](https://github.com/inokawa/virtua) and triggers the next page from a DOM end-anchor's bounding box. Lynx gives you two scroll primitives — and which one you pick is the whole performance story. ### `` mounts everything [``](https://lynxjs.org/api/elements/built-in/scroll-view) is the right tool for **short, heterogeneous** screens: Settings, Compose, a status thread, a media sheet. Every child stays mounted; you scroll a normal layout tree. That is also why it is the **wrong** tool for a timeline — once the federated feed grows past a few dozen statuses, you are paying for every card above and below the viewport. ### `` recycles and paginates [``](https://lynxjs.org/api/elements/built-in/list) is Lynx's recycling scroller. Off-screen `` cells are reused; you only keep a window of real nodes alive. Infinite scroll is a native event, not a geometry poll: ```vue ``` Three attributes do the heavy lifting: | Piece | Role | |-------|------| | `estimated-main-axis-size-px` | Lets the list lay out the scroll range before each cell measures | | `lower-threshold-item-count` | How many items from the end before we ask for more | | `@scrolltolower` | Fires `usePaginator.loadNext()` — Elk's masto.js `Paginator` iteration, unchanged | [`TimelinePaginator.vue`](https://github.com/huxpro/vue-lynx/blob/main/examples/elk/src/components/TimelinePaginator.vue) is the shared feed shell: first paint, error/retry, the recycling list, and a footer spinner / "End of the timeline". Timelines, Explore posts, hashtag pages, bookmarks/favourites, and profile Posts / Replies / Media all reuse it. Explore tags/news, notifications, followers, and search wire the same `` + `scrolltolower` pattern to their own item templates. `` still appears where recycling would not help — thread detail, settings forms, the compose sheet. The rule of thumb the port settled on: **if it can grow without bound, it is a ``; if it is a finite page, keep ``.** ## Why Elk is a serious test Elk is a Nuxt 3 app with ~196 components, 55 pages and 50 composables. Vue Lynx has no Nuxt (no SSR, file routing, Nitro, auto-imports) and no DOM — so instead of forking, the port **reuses Elk's framework-agnostic layers and rebuilds the UI on Lynx elements**: | Layer | Verdict | Notes | |-------|---------|-------| | **masto.js API client** | ✅ reused unpatched | wrapper-injected native `fetch` is synchronized with web `globalThis.fetch`; remaining constructors are targeted through `source.define`, with fill-if-missing native shims | | **Content pipeline** (`content-parse.ts`) | ✅ ~95% verbatim | ultrahtml sanitize + custom-emoji / markdown / mention-collapse transforms | | **Content renderer** (`content-render.ts`) | ♻️ retargeted | same AST walk; emits ``/`` runs with tap navigation instead of `

`/``/`RouterLink` | | **Paginator, timeline filters, status actions, search, cache** | ✅ reused | DOM scroll trigger → native `` `scrolltolower` | | **Virtual scrolling** | ♻️ replaced | Elk's DOM virtualizer (virtua) → Lynx's native recycling `` — *less* code | | **Routing** | ♻️ rebuilt | Nuxt file routes → explicit vue-router table on `createMemoryHistory`, same route shapes so content-renderer mention/hashtag rewrites work unchanged | | **Every template** | ♻️ rebuilt | `

`→``, ``→``, `@click`→`@tap`, Elk's exact theme palette as Lynx CSS vars | | **Icons** | ♻️ adapted | Elk's RemixIcon set (`i-ri:*`) rendered as tinted XML through Lynx's built-in `` element | | **Native safe area** | ✅ adapted | fullscreen iOS cards consume Sparkling `topHeight` / `bottomHeight` global props, with Lynx Explorer alias support | The full feature-parity checklist (including what's deliberately not ported and why — OAuth redirects, TipTap, PWA, Shiki, blurhash…) lives in [PRD.md](https://github.com/huxpro/vue-lynx/blob/main/examples/elk/PRD.md), the architecture map in [PORTING.md](https://github.com/huxpro/vue-lynx/blob/main/examples/elk/PORTING.md), and side-by-side screenshot comparisons against the original elk.zone in [screenshots/](https://github.com/huxpro/vue-lynx/blob/main/examples/elk/screenshots/README.md). ## Highlights * **The content renderer is the crown jewel**: Mastodon statuses arrive as sanitized HTML. Elk parses them into an AST and renders vnodes; the port keeps the parse step byte-for-byte and only swaps the vnode targets — custom emoji become inline ``, mentions/hashtags become tappable `` runs that push vue-router routes. * **Native virtualized timeline**: feeds use Lynx `` with `estimated-main-axis-size-px` and `@scrolltolower` (not ``); masto.js `Paginator` iteration and Elk's reorder/buffer logic drive `loadNext()` unchanged across timelines, explore, profiles, and search. * **Deep links**: pass `globalProps: { initialPath: '/mas.to/tags/caturday' }` to the LynxView and the app opens on that route — the same mechanism a host app would use for notification taps. * **Guest + token sessions**: browse anonymously like Elk's guest mode, or paste a personal access token in Settings to unlock home timeline, notifications, posting, boosts and favourites. --- url: /guide/hackernews.md --- # HackerNews To better test that all ecosystem ingredients can work together, we forked Evan You's classic [Vue HackerNews 2.0](https://github.com/vuejs/vue-hackernews-2.0) as a "real-world" validation benchmark for Vue Lynx. Since Vue Lynx is built on Vue 3, we used the [Vue HackerNews 3.0](https://github.com/raukaute/vue-hackernews-3.0) community port as our reference as well. This port validates four mainstream ecosystem libraries working together: Vue Router, Pinia, TanStack Vue Query, and Tailwind CSS. ### Tailwind CSS Version In the first iteration, we had AI rewrite the styles using Tailwind CSS (`@lynx-js/tailwind-preset`). ### CSS Version For this version, we had AI reuse the original SCSS files from the reference implementation as much as possible. Interestingly, the CSS version maintains the `max-width` centered layout from the reference on desktop. Try clicking the "fullscreen" button below to see this in action. Both HackerNews variants intentionally keep an explicit outermost ``. Vue Lynx forwards that wrapper's classes, styles, and events to the single native page root; it does not create another native element. This lets the examples exercise root-level Tailwind and SCSS styling in LynxExplorer. ## Lynx Engine Compatibility The regression was bisected across both the frontend bundle and Lynx Engine versions: | Bundle | Lynx Engine | Observed result | |---|---|---| | Tailwind 0.2.1 (implicit page root) | [3.6.0](https://github.com/lynx-family/lynx/releases/tag/3.6.0), [3.7.0](https://github.com/lynx-family/lynx/releases/tag/3.7.0) | Data loaded, but the active route remained invisible | | Tailwind 0.2.1 (implicit page root) | [3.8.1](https://github.com/lynx-family/lynx/releases/tag/3.8.1), [3.9.0](https://github.com/lynx-family/lynx/releases/tag/3.9.0) | Feed rendered | | CSS 0.2.1 (explicit page root, before this fix) | 3.6.0, 3.8.1, source-built 4.1 | Exact `990100 page ui not found` error | | CSS 0.2.6 / pre-fix current bundle | source-built 4.1 | Same `990100` error | :::warning Minimum inferred engine version Use **Lynx Engine 3.8.1 or later** for these examples. It is the oldest tested engine that rendered the full Tailwind application. Engines 3.6.0 and 3.7.0 have a separate route-visibility limitation even when the data request succeeds. The CSS error reproduced unchanged on 3.6.0, 3.8.1, and 4.1, so it was a frontend root-handling regression, not evidence that explicit `` requires Engine 4.1. The fixed bundle has been directly verified on the source-built 4.1 engine; support for 3.8.1 and 3.9.0 is inferred from this bisect and the fix's reuse of the already-existing native page, pending a direct rerun on those Explorer releases. ::: ## Overview of Changes Most code transferred directly — component logic, Vue Router, Composition API, and even ``/`` work identically. The Vue HackerNews 3.0 reference is a Webpack-based SSR app using Vue 3 + Vuex 4 + axios. Here's what we changed when porting to Lynx: | Aspect | Vue 3 Reference | Vue Lynx | |--------|-----------------|----------| | **Template elements** | `
`, ``, ``, `` | ``, ``, ``, tap handlers | | **Event handling** | `@click` | `@tap` | | **Routing history** | `createWebHistory()` | `createMemoryHistory()` | | **Scrolling** | Window scroll | `` component | | **State management** | Vuex 4 | Pinia | | **Data fetching** | axios | TanStack Vue Query | | **Build tool** | Webpack 5 + Express | Rspeedy | | **SSR** | Full SSR with hydration | None (Lynx doesn't use SSR) | ### Notable adaptations: * **No `v-html`**: Lynx has no HTML parser, so we use a `stripHtml()` utility to convert HTML comments to plain text * **``**: Wrapped with Vue Router's `custom` slot API since Lynx has no `` tags --- url: /guide/ifr-benchmarks.md --- import { Badge } from '@theme'; # IFR Benchmarks This page collects the measurement campaigns behind [Instant First-Frame Rendering](/guide/ifr.md) and Element Templates. Each campaign answers one question: | campaign | the question it answers | |---|---| | [1. Strategy ladder](#1-strategy-ladder) | How much does the synchronous main-thread render **cost**, per design — and what is the ceiling? | | [2. All-examples sweep](#2-all-examples-sweep) | What does IFR **cost in size and TTI** on real apps, and is it semantically safe? (Also: proof that a single process cannot measure its benefit.) | | [3. Real threads](#3-real-threads) | What does IFR actually **win**, measured across a genuine thread boundary — with ReactLynx as the control? | | [3b. Large-app reevaluation](#3b-large-app-reevaluation) | Does the −19% median still hold once TodoMVC / Hacker News / AI Chat / Elk join the matrix? | **A note on configuration names.** Campaigns 1 and 2 ran before `enableIFR` defaulted to enabling Element Templates. Columns are labeled with today's product configurations: what the raw reports call `ifr` is today's **IFR without ET** (`enableElementTemplates: false` opt-out), and `ifr+et` is today's **default `enableIFR: true`**. Campaign 3b always sets both flags explicitly. Full methodology and raw data: [strategy benchmark](https://github.com/Huxpro/vue-lynx/blob/main/packages/ifr-bench/REPORT.md), [all-examples sweep](https://github.com/Huxpro/vue-lynx/blob/main/packages/ifr-bench/EXAMPLES-REPORT.md), [real-browser verification](https://github.com/Huxpro/vue-lynx/blob/main/packages/ifr-bench/VERIFICATION.md), [large-app reevaluation](https://github.com/Huxpro/vue-lynx/blob/main/packages/ifr-bench/reeval/REEVALUATION.md). ## 1. Strategy ladder > **Takeaway: Element Templates are the inflection point of render cost. > Plain IFR replay costs as much JS as a full render (~8–11 ms per 1000 > elements under an interpreter); ET cuts it about 6–15× across reruns; a > Vapor-style design would buy another 2–3× and sit at the PAPI floor.** This > is why ET is enabled by default with IFR: it attacks the one real cost IFR > adds to the main thread — the synchronous first-screen render inside > `loadTemplate`. Seven rendering strategies were prototyped and measured against the same logical first screens through the same Element PAPI surface. All variants produce byte-identical rendered documents. Scenes are ~1000–1400 elements: **static-heavy** (99.7% template-static), **content** (card feed, 37%), **list** (v-for, 12%). ### Warm render time, `--jitless` V8 (interpreter ≈ main-thread engine proxy) | variant | static-heavy | content | list | |---|---|---|---| | bg-baseline (No IFR pipeline) | 11.86 ms | 9.36 ms | 6.87 ms | | IFR without ET (shipped) | 10.94 ms | 8.40 ms | 6.57 ms | | ifr-direct (prototype) | 8.17 ms | 6.36 ms | 4.89 ms | | ifr-static-tpl (prototype) | 1.11 ms | 5.29 ms | 4.00 ms | | **IFR + ET (shipped, default)** | **0.74 ms** | **1.26 ms** | **1.44 ms** | | ifr-vapor (prototype upper bound) | 0.53 ms | 0.55 ms | 0.46 ms | | papi-floor (reference) | 0.54 ms | 0.39 ms | 0.31 ms | ### Cold first run, `--jitless` (models a device's one-shot first frame) | variant | static-heavy | content | list | |---|---|---|---| | IFR without ET | 18.0 ms | 16.4 ms | 11.7 ms | | **IFR + ET** | **4.2 ms** | **5.3 ms** | **4.9 ms** | | ifr-vapor | 1.4 ms | 1.2 ms | 0.8 ms | ### Ops payload that would cross the thread boundary | variant | static-heavy | content | list | |---|---|---|---| | without ET | 77.6 KB | 60.4 KB | 45.1 KB | | **with ET** | **69 B** | **9.2 KB** | **17.1 KB** | The prototypes are kept for reference: `ifr-direct` was superseded by ET (handle-based application is implied by template instantiation), and the Vapor design remains the tracked endgame — its prototype omits reactive-effect bookkeeping, so read its numbers as an optimistic bound. ## 2. All-examples sweep > **Takeaway: this campaign measures IFR's *costs* and its *semantic > safety* — not its benefit. Bundle gzip ×2.26 and a TTI upper bound of > ×1.36 are the price; 22/23 examples rendering byte-identical documents > across all configurations is the safety result. The per-example FCP > columns come out flat (median ×1.04) — which is itself a finding: IFR is > a dual-thread architecture optimization, and a single-process harness has > no background boot or IPC to remove. Read the flat FCP as proof that the > JS work is conserved, then see campaign 3 for the actual win.** Every example was built in three configurations and executed as real bundle halves in a PAPI-over-jsdom environment (5 runs each, medians, `--jitless`, main-thread parse excluded as lepus ships precompiled bytecode). | example | No IFR | IFR w/o ET | Δ | IFR + ET | TTI No IFR | TTI IFR w/o ET | ΔTTI | nodes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 7guis | 65.6 | 63.1 | −4% | 64.3 | 65.6 | 76.0 | +16% | 169 | | basic | 32.0 | 34.3 | +7% | 33.7 | 32.0 | 44.0 | +38% | 9 | | css-features | 43.3 | 50.0 | +15% | 44.2 | 43.3 | 63.3 | +46% | 51 | | gallery | 66.6 | 65.0 | −2% | 69.5 | 66.6 | 89.4 | +34% | 4 | | hackernews-css | 40.0 | 35.4 | −11% | 38.9 | 40.0 | 58.8 | +47% | 14 | | hackernews-tailwind | 55.9 | 53.0 | −5% | 52.2 | 55.9 | 76.1 | +36% | 20 | | hello-world | 33.5 | 35.6 | +6% | 36.0 | 33.5 | 45.7 | +36% | 16 | | keep-alive | 47.7 | 44.5 | −7% | 46.5 | 47.7 | 57.0 | +19% | 56 | | main-thread | 26.7 | 24.3 | −9% | 24.5 | 26.7 | 33.0 | +24% | 3 | | networking | 24.9 | 30.5 | +23% | 35.1 | 24.9 | 30.5 | +23% | 0 | | option-api | 53.8 | 57.1 | +6% | 59.7 | 53.8 | 68.8 | +28% | 9 | | pinia | 40.1 | 38.7 | −4% | 39.4 | 40.1 | 50.0 | +25% | 19 | | provide-inject | 32.2 | 34.3 | +6% | 37.9 | 32.2 | 43.7 | +36% | 13 | | reactivity | 33.6 | 36.1 | +7% | 36.0 | 33.6 | 46.1 | +37% | 19 | | slots | 40.1 | 43.8 | +9% | 39.0 | 40.1 | 54.3 | +35% | 47 | | suspense | 42.3 | 38.4 | −9% | 38.3 | 42.3 | 51.1 | +21% | 16 | | swiper | 30.3 | 28.5 | −6% | 29.8 | 30.3 | 41.1 | +36% | 47 | | tailwindcss | 22.5 | 25.8 | +15% | 20.9 | 22.5 | 36.9 | +64% | 71 | | todomvc | 17.9 | 21.6 | +21% | 20.7 | 17.9 | 31.4 | +76% | 11 | | todomvc-day1 | 20.4 | 20.1 | −2% | 15.9 | 20.4 | 29.4 | +44% | 7 | | transition | 45.9 | 45.2 | −1% | 47.7 | 45.9 | 59.7 | +30% | 56 | | v-model | 39.1 | 40.9 | +4% | 40.7 | 39.1 | 52.3 | +34% | 26 | | vue-router | 43.2 | 45.4 | +5% | 45.0 | 43.2 | 60.3 | +40% | 6 | (FCP/TTI in ms. `networking` renders 0 nodes at first frame — fetch-driven, the documented "don't enable IFR" profile. `todomvc-codex` was thread-incompatible at measurement time and has since been fixed at the example level.) ### Bundle size (gzip, KiB) — the headline cost | example | No IFR | with IFR | Δ | | --- | --- | --- | --- | | hello-world | 34.5 | 76.3 | +121% | | gallery | 37.2 | 80.0 | +115% | | hackernews-css | 72.6 | 179.1 | +147% | | networking | 57.5 | 145.5 | +153% | | tailwindcss | 36.2 | 79.0 | +118% | | **median (all examples)** | | | **×2.26** | The main-thread bundle section grows from ~17 KiB (worklet registrations only) to 78–195 KiB (Vue runtime + app copy); ET adds ~1% on top. The sweep also surfaced and fixed three real bugs (`SystemInfo` clobbering, numeric-style baking bypassing auto-px normalization, CSS Modules crashing the main-thread bundle) — running every real app shape through the dual-thread pipeline is worth doing for correctness alone. ## 3. Real threads > **Takeaway: with a genuine thread boundary, IFR wins on content-first > screens — FCP about −8% to −26% depending on the example set and host, > with campaign medians of −19% (ten demos) and −12% (later seven-app mix). > The win comes from removing background boot + IPC from the critical path. > On small screens the two IFR configurations (with and without ET) are > usually within a few percent on web FCP: ET's clearest measured advantages > remain render cost and ops payload (campaign 1) and native cold starts.** Setup: Lynx for Web — the background runtime runs in a genuine Web Worker with real postMessage IPC, in headless Chromium. FCP = `` insertion → first painted content, medians of 7 fresh browser contexts. Both throttle levels below come from one session measured under the **current** flag semantics (`enableIFR: true` = IFR + ET; explicit opt-out = IFR without ET). Absolute milliseconds drift with host load between sessions; within-run ratios are the stable currency. ### The ReactLynx control ReactLynx has no IFR-off switch, so "off" is emulated faithfully (empty main-thread first screen, full background render + hydration). First, in the **same single-process harness as campaign 2**, ReactLynx is just as flat as Vue Lynx — the reference implementation cannot demonstrate its own headline feature without a thread boundary either: | ReactLynx probe (single process, jitless) | FCP | TTI | |---|---|---| | IFR on | 25.3 ms | 32.7 ms | | IFR off (emulated) | 24.3 ms | 24.3 ms | On real threads both frameworks win, by the same class of margin: the ReactLynx probe (85 nodes) improves 97.7 → 75.2 ms (**−23%**), squarely inside the range the Vue examples below span. ### Full matrix — no CPU throttle | example (nodes) | No IFR | IFR w/o ET | Δ | IFR + ET (default) | Δ | |---|---|---|---|---|---| | hello-world (16) | 96.6 | 75.8 | −22% | 75.4 | −22% | | todomvc-day1 (7) | 92.9 | 78.6 | −15% | 73.4 | −21% | | swiper (39) | 100.5 | 77.7 | −23% | 79.1 | −21% | | tailwindcss (68) | 95.7 | 80.9 | −15% | 80.6 | −16% | | keep-alive (49) | 103.5 | 86.0 | −17% | 84.4 | −18% | | transition (55) | 104.6 | 93.3 | −11% | 85.1 | −19% | | 7guis (145) | 104.1 | 90.3 | −13% | 80.6 | −23% | | gallery (302) | 136.6 | 104.5 | −23% | 109.9 | −20% | | css-features (50) | 102.0 | 93.9 | −8% | 100.3 | −2% | | hackernews-css (18) | 115.3 | 131.7 | **+14%** ⚠ | 131.8 | **+14%** ⚠ | (FCP in ms. Medians: **−19%** default, −15% without ET — the per-example differences between the two configurations flip between runs and should be read as noise. The Hacker News row here pre-dates shell IFR; see [§3b](#3b-large-app-reevaluation).) ### 4× CPU throttle | example | No IFR | IFR w/o ET | Δ | IFR + ET | Δ | |---|---|---|---|---|---| | hello-world | 344.1 | 282.8 | −18% | 289.1 | −16% | | todomvc-day1 | 313.2 | 281.7 | −10% | 280.6 | −10% | | swiper | 333.1 | 304.9 | −8% | 298.4 | −10% | | tailwindcss | 339.9 | 330.7 | −3% | 316.4 | −7% | | keep-alive | 391.4 | 366.6 | −6% | 370.8 | −5% | | gallery (302 nodes) | 437.2 | 430.5 | −2% | 434.5 | −1% | | css-features | 352.0 | 353.6 | +0% | 346.4 | −2% | | transition | 350.6 | 355.2 | +1% | 367.6 | +5% | | 7guis | 352.1 | 366.8 | +4% | 355.0 | +1% | | hackernews-css | 324.0 | 352.9 | **+9%** ⚠ | 360.0 | +11% | | ReactLynx probe | 331.6 | 308.0 | −7% | — | | Two readings from this campaign: 1. **Content-first screens keep a win under throttling** on this set (hello-world −16…−18%, todomvc-day1 −10%, swiper −10%), while screens whose FCP is dominated by CSS processing or big bundles compress toward zero — a throttled CPU multiplies the bundle-parse term that the web platform pays on the FCP path. 2. **The inversion profile reproduces at both throttle levels** when the first paint has little sync chrome. The Hacker News row above paid +14% at full speed and +9…11% throttled while the example skipped the IFR mount entirely. After shell IFR (always mount; gate fetches), the same app flipped to about **−12%** at full speed — see §3b. On native, precompiled lepus bytecode shrinks the parse term inversions are made of. ### How far is this from plain single-threaded web? Vanilla single-threaded baselines (plain Vue via `@vue/runtime-dom`; plain Preact via ReactLynx's exact fork pointed back at the real DOM) decompose the remaining gap. On the ReactLynx probe screen, no throttle: | component | cost | evidence | |---|---|---| | render 85 elements + paint (preact on DOM) | ~21 ms | plain-preact, framework pre-parsed | | + framework fetch+parse on the FCP path | +7 ms | cold − warm | | + the Lynx-for-Web platform layer | +47 ms | rl-ifr − plain-preact cold | | + background boot + hydration IPC round-trip | +23 ms | rl-noifr − rl-ifr | The thread boundary costs ~23 ms and **IFR removes exactly that slice**. The remaining gap vs plain web is the web-host emulation layer — a per-host constant that native Lynx, whose platform layer is native code, does not pay. ## 3b. Large-app reevaluation > **Takeaway: keep the campaign-3 story for small content-first screens, and > widen the range once product-sized apps join.** A second Lynx-for-Web pass > (explicit `off` / `et` / `ifr` / `ifr-et` flags; hello-world, TodoMVC, > gallery, Hacker News, AI Chat, Elk) landed content-first medians around > **−12%** at full speed — inside the same −8%…−26% band as campaign 3's > per-example spread, but a lower suite median than −19%. Under 4× throttle, > Elk / AI Chat regress about **+25% to +44%**; the all-seven median flips > slightly positive. Bundle gzip stays about **×2.2–2.5** (median ×2.23 here > vs ×2.26 in campaign 2). | source | set | IFR + ET FCP vs No IFR (no throttle) | notes | |---|---|---|---| | Campaign 3 (Claude Code) | 10 mostly small examples | **−8% to −23%, median −19%** | ReactLynx control −23% | | Campaign 3b (Cursor) | 7 apps incl. TodoMVC, gallery, HN, AI Chat, Elk | content-first **~−12%** (hello **−26%**); all-seven median **−12%** with HN after shell IFR | absolute ms lower on a different host; compare ratios | | Campaign 3b @ 4× throttle | same 7 | content-first **~−6%**; all-seven median **~+3%** | Elk +44%, AI Chat +25% | ### Focus matrix — no CPU throttle (campaign 3b) Four explicit configs. FCP ms, medians of 7. `ifr` = IFR with `enableElementTemplates: false`. | example | nodes† | off | et | ifr | ifr+et | ifr+et Δ | |---|---:|---:|---:|---:|---:|---:| | hello-world | 16 | 63.6 | 60.0 | 48.6 | **47.0** | **−26%** | | todomvc-day1 | 7 | 55.4 | 56.9 | 53.1 | **50.3** | **−9%** | | todomvc | 11 | 63.2 | 71.6 | **49.1** | 55.9 | −12% | | gallery | 304 | 74.7 | 75.8 | **61.3** | 62.3 | −17% | | hackernews-css (shell IFR) | 347 | 71.5 | — | 64.1 | **63.0** | **−12%** | | ai-chat | 104 | 90.4 | 90.3 | **74.9** | **74.9** | **−17%** | | elk | 956 | 100.3 | 99.6 | 96.8 | 100.2 | **−0%** | † Settled node count after hydration / fetch. Hacker News remounted with shell IFR in this pass (earlier campaign-3 row skipped `app.mount()` on the main thread and inverted). AI Chat / Elk leave IFR off in-tree; numbers are forced-on for the matrix. ### Same set — 4× CPU throttle | example | off | ifr | ifr+et | ifr+et Δ | |---|---:|---:|---:|---:| | hello-world | 163.6 | 158.1 | **141.1** | **−14%** | | todomvc-day1 | 157.3 | 160.5 | **147.2** | **−6%** | | todomvc | 168.4 | 157.9 | 159.0 | −6% | | gallery | 206.6 | 206.0 | 213.0 | +3% | | hackernews-css | 172.5 | 183.5 | 186.7 | +8% | | ai-chat | 200.9 | 239.6 | 251.1 | **+25%** | | elk | 235.6 | 338.6 | 340.0 | **+44%** | **Reading across §3 and §3b:** treat **−12% to −19%** as the plausible content-first median band on Lynx for Web, not a single headline −19%. ET-only remains roughly free on size (~+1%) and flat on web FCP; keep defaulting ET with IFR for the strategy-ladder render-cost win (about **6–15×** across reruns), not because it moves small-screen web FCP. Raw JSON and write-up: [packages/ifr-bench/reeval/](https://github.com/Huxpro/vue-lynx/tree/main/packages/ifr-bench/reeval). ## 4. Native engine observations The full example suite was validated on a native simulator (LynxExplorer, Lynx SDK 1.4 / PrimJS): 24/25 examples pass with **zero hydration mismatches**, and No-IFR vs IFR+ET screens are visually identical (SSIM ≥ 0.9977; scoped CSS, baked inline styles, and auto-px semantics all correct on the native engine). Single-sample cold-start recordings show first content ~0.3 s earlier (gallery) up to ~1.2 s earlier (7guis) with IFR — directional confirmation of the mechanism, not a stable benchmark. ## Reproduce ```bash # strategy ladder + correctness oracle pnpm --filter vue-lynx-ifr-bench run check pnpm --filter vue-lynx-ifr-bench run bench # all-examples sweep node packages/ifr-bench/examples-sweep/orchestrate.mjs node packages/ifr-bench/examples-sweep/sweep.mjs # real-browser (Lynx for Web) measurement, with plain-web baselines node packages/ifr-bench/web-harness/run-browser.mjs [runs] [throttle] # focused 4-config rebuild (incl. ai-chat / elk) used for §3b node packages/ifr-bench/reeval/orchestrate-focused.mjs ``` --- url: /guide/ifr.md --- import { Badge } from '@theme'; # Instant First-Frame Rendering (IFR) Instant First-Frame Rendering (IFR) displays real content during `loadTemplate`, before the background thread starts. Vue Lynx pairs IFR with Element Templates, which reduce the work required to create static-structure subtrees. Together they remove the blank-frame wait and make the synchronous main-thread render substantially cheaper. ## Configuration Enable IFR in `lynx.config.ts`: ```ts title="lynx.config.ts" import { defineConfig } from '@lynx-js/rspeedy' import { pluginVueLynx } from 'vue-lynx/plugin' export default defineConfig({ plugins: [ pluginVueLynx({ enableIFR: true, }), ], }) ``` Element Templates are enabled automatically when IFR is enabled. No app-code changes are required for a typical component tree. | configuration | `enableIFR` | `enableElementTemplates` | purpose | | --- | ---: | ---: | --- | | No IFR | `false` | `false` | Default background-thread rendering | | IFR + ET | `true` | omitted or `true` | Recommended IFR path | | IFR without ET | `true` | `false` | Compatibility, debugging, and measurement opt-out | To isolate an Element Templates issue while keeping IFR active, opt out explicitly: ```ts title="lynx.config.ts" pluginVueLynx({ enableIFR: true, enableElementTemplates: false, }) ``` Element Templates can also be enabled independently for ordinary background rendering with `enableElementTemplates: true`. This is an advanced composition, not the recommended IFR setup. ## Why IFR removes the blank frame Without IFR, visible content waits for the background thread to boot, evaluate the application, render Vue's tree, and send an ops batch back to the main thread: ```text Main Thread: empty page ─────────────────────────▶ apply ops ─▶ paint Background Thread: boot ─▶ render ─▶ IPC ──┘ ``` With IFR, the main-thread bundle carries the Vue runtime and application code. Vue renders synchronously inside `loadTemplate`, while the background thread starts in parallel: ```text Main Thread: loadTemplate ─▶ render ─▶ paint Background Thread: boot ─▶ render ─▶ hydrate ``` The first screen therefore appears before any background JavaScript has run. After hydration, the background thread owns the tree and all later updates use the normal ops pipeline. ## Hydration Both threads execute the same application with the same initial data. The main-thread render records every ops batch it applies. The background thread's initial batches are reconciled against that recording: * Identical batches are skipped because their result is already on screen. * Text, style, and attribute differences are patched in place. * Structural differences remove the first-screen tree and rebuild it from the background ops. Correctness does not depend on both renders matching. A structural mismatch only loses the IFR performance benefit and produces an `[vue-lynx] IFR hydration mismatch` warning in development. Event signs and element IDs are deterministic across both renders. Once the background handler registry is ready, events from first-frame elements route to the normal Vue handlers without rebinding. ## Element Templates IFR changes **when** the first frame is rendered. Element Templates reduce **how much work** that render performs. In the ordinary path, each static element still creates a vnode and shadow node, emits several ops frames, crosses the thread boundary, and reaches the main-thread ops interpreter. The compiler can collapse an eligible subtree: ```text Without ET: vnode → ShadowElement → per-node ops → interpreter → PAPI With ET: one vnode → INSTANTIATE_TEMPLATE → straight-line PAPI create() ``` The static skeleton becomes a compiler-generated JavaScript `create()` function. Vue sends one `INSTANTIATE_TEMPLATE` op for the subtree; dynamic text, classes, styles, and attributes are holes updated through the ordinary `SET_*` ops. ### What gets lowered A subtree is eligible when its structure is known at compile time: every node is a plain Lynx element and only property values or text content are dynamic. ```vue {{ title }} ... ``` Structural features stay on the normal vnode path: * components, slots, and `v-if` / `v-for` hosts; their plain-element bodies can still be lowered * runtime directives, refs, keys, IDs, and vnode hooks on interior nodes * `` and ``, which have dedicated main-thread behavior * comments and mixed dynamic text runs The template root retains its normal vnode props and directives. Scoped CSS is supported: the compiler bakes the component CSS scope ID into each lowered element. Static inline styles are baked only when their semantics are known to match the ordinary style path. ### Framework templates, not binary engine templates Vue Lynx does not currently use Lynx's binary `elementTemplates` bundle section, `__ElementFromBinary`, or `__GetTemplateParts`. Its Element Templates are a framework-level optimization built from ordinary typed Element PAPI calls. This removes vnode, per-node ops, serialization, and interpreter dispatch while preserving the existing renderer and hydration protocol. A binary-template backend could consume the same static-skeleton and hole metadata in the future, but it is not part of this implementation. ### Semantics guarantee Lowering is an optimization, not a rendering mode. Ineligible structures fall back automatically, and lowered and unlowered trees must produce the same rendered document and updates. Interior nodes do not carry Vue's selector bookkeeping attributes, but any element with a `ref`, ID, or other identity requirement is kept out of the anonymous interior path. ## Writing IFR-friendly first screens The initial render executes once per thread, so it should behave like a pure function of its inputs: * Keep first-screen output deterministic. Avoid `Math.random()`, `Date.now()`, or thread-dependent branching in render paths. * Put data fetching, timers, subscriptions, and other side effects in Composition API lifecycle hooks. These hooks are suppressed during the main-thread render and run on the background thread. * Options API `mounted()` is not yet suppressed on the main thread. Prefer Composition API hooks for first-screen components that use IFR. * IFR paints only synchronously available data. A fetch-driven screen should still **mount** and render a useful shell or skeleton on the main thread — gate the network work (for example `useQuery({ enabled: !isIfrMainThread() })`), do not skip `app.mount()` entirely. Skipping the mount keeps the larger IFR bundle with none of the first-paint benefit. * Regular events can be dropped in the short interval before the background registry is ready. [Main Thread Script](/guide/main-thread-script.md) handlers remain interactive because they already live on the main thread. ## Trade-offs * IFR places the Vue runtime and application in both thread bundles. Across the example suite, `main.lynx.bundle` gzip size grows by about 2.26x at the median. * The application evaluates on both threads. The all-examples serial-work TTI proxy grows by about 35–36%; on a device, main-thread rendering overlaps with background startup. * IFR cannot accelerate content that does not exist until an asynchronous request completes. * CSS Modules class names can be un-hashed on the first frame and patched to their final names during hydration. Measure bundle-sensitive and fetch-driven screens before enabling IFR. For content-first screens with synchronous initial data, IFR + ET is the recommended configuration. ## Benchmarks The table keeps the three product configurations separate. FCP is hello-world on Lynx for Web with a real Web Worker (no CPU throttle). Render cost is the warm median for a roughly 1,000-element dynamic-content scene under V8 `--jitless`. TTI and gzip are medians from the all-examples sweep, normalized to No IFR. These are separate campaigns; the columns should not be read as one end-to-end trace. | configuration | flags | FCP (hello-world) | render cost | TTI proxy | bundle gzip | | --- | --- | ---: | ---: | ---: | ---: | | No IFR | `IFR=false, ET=false` | 96.6 ms | 9.4 ms | 1.00x | 1.00x | | **IFR + ET** | `IFR=true, ET=default` | **75.4 ms (−22%)** | **1.3 ms** | 1.35x | ~2.26x | | IFR without ET | `IFR=true, ET=false` | 75.8 ms (−22%) | 8.4 ms | 1.36x | ~2.26x | Across campaigns, the **content-first** Lynx-for-Web FCP win for default IFR + ET sits roughly in **−12% to −26%** (medians −19% on a ten-example demo suite; −12% on a later seven-app set that adds TodoMVC / gallery and larger apps). Absolute milliseconds drift by host; ratios are the stable currency. See [IFR Benchmarks](/guide/ifr-benchmarks.md) for both runs. ### What the data says 1. **IFR's win is real and structural on content-first screens.** With a genuine thread boundary (Lynx for Web), IFR removes background-thread boot * IPC from the critical path — which is also why a single-process benchmark cannot see it. One campaign (ten mostly small examples) saw **−8% to −23%, median −19%**; a later reevaluation including TodoMVC, gallery, Hacker News (shell IFR), AI Chat, and Elk saw content-first medians around **−12%** at full speed (hello-world alone about −22% to −26% depending on host). ReactLynx's control on the first harness was −23%, the same class of win. 2. **Element Templates attack the one cost IFR adds.** The synchronous main-thread render gets about **6–15×** cheaper across strategy-ladder reruns, and the ops payload shrinks 3–1,000× (a 1,400-element static screen ships 69 bytes instead of 78 KB). On web FCP for small screens this often does not show — the two IFR configurations are usually within a few percent — but the render-cost term grows with screen size and shrinks with CPU speed, which is why ET defaults on with IFR. 3. **The price is bundle size, and large / fetch-heavy apps can invert.** `main.lynx.bundle` gzip grows about **×2.2–2.5** (suite median ~×2.26). On the web, bundle parse sits on the FCP path: under 4× CPU throttle, big apps (Elk, AI Chat) have measured **+25% to +44%** FCP with IFR, while small content-first screens still keep a thinner win. Paint a real shell under IFR; do not skip `app.mount()` just because data is async (Hacker News recovered from about **+38% to −12%** at full speed after switching to shell IFR). ### Recommendations | your first screen | do this | | --- | --- | | content-first, renders from synchronous data | **enable `enableIFR: true`** (brings ET) — typical web FCP win about **−12% to −26%** on content-first sets | | data-driven with a sync shell / skeleton | enable; gate fetches off the IFR main-thread pass | | fetch-driven with **nothing** useful to paint before a response | don't enable; or add a real shell first, then enable | | large bundle on web + slow CPU | measure — throttle can erase or reverse the win | | bundle-size-critical | measure first — gzip roughly doubles | And regardless of profile: keep the first screen deterministic and put side effects in Composition API lifecycle hooks (see [Writing IFR-friendly first screens](#writing-ifr-friendly-first-screens)); reach for `enableElementTemplates: false` only to bisect a suspected ET issue — not as a performance setting. The full measurement campaigns — the seven-variant strategy ladder, per-example FCP/TTI/size across the whole example suite, the real-browser dual-thread comparison against ReactLynx, a later large-app reevaluation, and the single-threaded plain-web decomposition — are on the dedicated [IFR Benchmarks](/guide/ifr-benchmarks.md) page. --- url: /guide/introduction.md --- # What is Vue Lynx? **Vue Lynx** is a Vue 3 custom renderer for [Lynx](https://lynxjs.org), enabling you to build native Lynx applications using Vue's familiar Composition API, single-file components, and reactive data model. If you know Vue 3 and are familiar with Lynx, you already know how to use Vue Lynx. ## Main Features * **Familiar Vue 3 API** — Use `ref()`, `computed()`, `v-for`, `v-if`, SFC ` ``` If your `onMounted` only sets up reactive state, timers, or data fetching, no `nextTick` is needed — those don't depend on native elements. #### Main Thread Script for zero-latency interactions (advanced) From the background thread, native element access is always asynchronous. For most use cases this is fine. Template `ref` returns a `ShadowElement` (a background-thread reference to the native element), and layout queries like `getBoundingClientRect()` are available via an [async API](https://lynxjs.org/next/api/lynx-api/nodes-ref.html): ```vue ``` When you need synchronous access — smooth animations, gesture handling, layout measurement — use [Main Thread Script](/guide/main-thread-script.md). Functions marked `'main thread'` run directly on the main thread and can call [Main Thread APIs](https://lynxjs.org/next/api/lynx-api/main-thread.html) like [`getComputedStyleProperty()`](https://lynxjs.org/next/api/lynx-api/main-thread/element-get-computed-style.html), listen to `main-thread-bindlayoutchange`, and access native elements via `useMainThreadRef()`: ```vue ``` See [Vue Features Compatibility](/guide/vue-compatibility.md) for feature-specific caveats and unsupported features. ## Next Steps * [Vue Compatibility](/guide/vue-compatibility.md) — Feature-by-feature breakdown of what works and what differs * [Main Thread Script](/guide/main-thread-script.md) — Run performance-critical logic on the main thread * [Tutorial: Product Gallery](/guide/tutorial-gallery.md) — Build a waterfall gallery with tap-to-like, auto-scroll, and a Main Thread Script scrollbar * [Tutorial: Product Detail](/guide/tutorial-swiper.md) — Build a touch-swipeable image carousel with snap animation --- url: /guide/main-thread-script.md --- # Main Thread Script The Main Thread Script is a JS script that can be executed on the main thread. The most common use cases are smooth animations and gesture handling. It addresses the response delay inherent in Lynx's multi-threaded architecture, aiming to achieve a near-native interactive experience. ## Event Response Delay in Lynx Here is a simple animation: a small square that moves in sync with a `scroll-view`. In the component, we listen to the scroll event, retrieve the current scroll position from the event parameters, and update the square's position immediately: You can try scrolling the scroll-view on the left side of the example. The blue square on the right will follow the scroll-view's movement. However, you might notice that its movement has an unpredictable delay, especially on devices with lower performance. This delay also increases as the complexity of the page increases. This is because in Lynx's architecture, events are triggered on the main thread, while regular JS event handlers (Vue's `@scroll`, etc.) can only be executed on background threads. Therefore, using regular events to trigger animations introduces multiple thread crossings, resulting in untimely responses and animations lagging behind gestures. ``` Without Main Thread Script: ┌────────────────┐ ┌────────────────────┐ ┌────────────────┐ │ Main Thread │ │ Background Thread │ │ Main Thread │ │ (event fires) │ ──▶ │ (Vue handler runs) │ ──▶ │ (render) │ └────────────────┘ └────────────────────┘ └────────────────┘ ▲ │ └─────────────── 2 thread crossings ─────────────────┘ ``` The main thread script provides the capability to handle events synchronously on the main thread, ensuring synchronous event responses. ``` With Main Thread Script: ┌────────────────────────────────────────────────────────────┐ │ Main Thread │ │ event fires ──▶ handler runs ──▶ render │ └────────────────────────────────────────────────────────────┘ 0 thread crossings ``` ## Use Main Thread Functions to Eliminate Event Response Delay ### Implementing Animations with Main Thread Script Synchronizing events using main thread script is very simple. Here we try to modify the previous example. First, we inform the framework that we want to handle this event on the main thread by using the `main-thread-bindscroll` attribute instead of `@scroll`: ```vue ``` Since `onScroll` is now a main thread event handler, we also need to declare it as a main thread function. This is done by adding a `'main thread'` directive as the first line inside the function body: ```ts const onScroll = (event) => { 'main thread' // ... } ``` After declaring it as a main thread function, we can no longer call it from the background thread. Finally, we can now directly manipulate the element's properties on the main thread, so there's no need to use a reactive `ref` to change the position. When using a main thread function as an event handler, we can obtain a reference to the target element using `useMainThreadRef()` and access it via `.current` inside the main thread function. This object allows you to synchronously get and set node properties, such as using `setStyleProperty()` in the example: ```vue ``` That's all the changes needed. The example below places the components before and after the modification side by side for comparison. You may notice that the animation delay has disappeared! ## Retrieving Data from the Background Thread You may have noticed that designating a function as a main thread function isolates it from its surrounding context, making it feel like an "island." Its runtime environment is different from other functions, meaning it cannot freely communicate with the background thread. However, obtaining data from the background thread inside a main thread function is straightforward: just use it directly, as if it were a normal function. ```vue ``` When the main thread function is defined, it automatically captures external variables from the background thread, such as the `red` variable in the example above. However, you cannot directly modify the values in the background thread. The values captured by the main thread function are not updated in real time. Instead, they are synchronized from the background thread to the main thread only after the component containing the main thread function re-renders. Additionally, the synchronization requires that the captured values be serializable using `JSON.stringify()`. To summarize the precautions: * Main thread functions can and must only run on the main thread. Main thread functions can call each other. * Captured variables need to be passed between threads using `JSON.stringify()`, so they must be serializable to JSON. * Main thread functions do not support nested definitions. * You cannot modify variables captured from the external scope within a main thread function. ## Using `main-thread-ref` to Obtain Node Objects In the example above, clicking on the view would change its background color. If we want to change the color of only a specific child element, it is not easy to achieve with just `event.target` and `event.currentTarget`. In this case, you can use `main-thread-ref` to obtain a node object usable on the main thread. Create a `MainThreadRef` using the `useMainThreadRef()` composable, and then assign it to the target node's `main-thread-ref` attribute: ```vue ``` Note that the `current` property of `MainThreadRef` can only be accessed within a main thread function. ## Maintaining State in Main Thread Functions Main thread functions cannot modify captured variables. Therefore, if you need to maintain state between main thread functions, you should use `MainThreadRef`: ```vue ``` ## Cross-Thread Function Calls The examples so far use `'main thread'` functions as event handlers. But what if you need to call a main thread function from the background thread, or vice versa? Vue Lynx provides `runOnMainThread()` and `runOnBackground()` for bidirectional async communication. In this example, tapping the box triggers a round trip: 1. **Main Thread** `onTap` fires, calls `runOnBackground(incrementCount)()` to update reactive state 2. **Background Thread** `watch(count)` fires, calls `runOnMainThread(applyColor)(nextColor)` to change the box color ### Asynchronously Invoking Main Thread Functions from the Background Thread Use `runOnMainThread()` in the background thread to asynchronously execute a main thread function on the main thread: ```vue ``` ### Asynchronously Invoking Non-Main Thread Functions from the Main Thread Use `runOnBackground()` on the main thread to asynchronously execute a regular function on the background thread: ```vue ``` ## Cross-Thread Shared Modules By default, main thread functions cannot directly call plain functions that do not have the `'main thread'` directive. This makes code reuse difficult. To address this limitation, Vue Lynx supports the `shared-module` mechanism, allowing you to explicitly declare certain modules as shareable between the main thread and the background thread. Add `with { runtime: 'shared' }` after the `import` statement, and the exported variables (including functions, classes, objects, etc.) in that module can be directly called in main thread functions: ```ts import { func } from './utils' with { runtime: 'shared' } const onTap = () => { 'main thread' func() // ✅ Allows calling plain functions in a main thread function } ``` > \[!NOTE] > Functions in modules imported with `with { runtime: 'shared' }` can be called by main thread functions, but they are not automatically treated as main thread functions. If they use main-thread-specific capabilities like `MainThreadRef`, they must still be manually marked with `'main thread'`. The shared module is a regular TypeScript/JavaScript file — it does **not** need a `'main thread'` directive. It must contain only plain functions and constants (no Vue reactivity, no DOM access, no side effects that depend on a specific thread). ```ts // color-utils.ts — shared between both threads const COLORS = ['#4FC3F7', '#81C784', '#FFB74D', '#E57373', '#BA68C8'] export function getNextColor(index: number): string { return COLORS[index % COLORS.length]! } ``` Once imported with `{ runtime: 'shared' }`, the exported functions can be called both inside `'main thread'` functions and in regular background thread code: ```vue ``` ### Referencing Third-Party Libraries Usually, third-party libraries (e.g., `motion-dom`) contain plain JavaScript functions. If called directly in a main thread function, they will report an error due to the missing `'main thread'` directive. We can use `with { runtime: 'shared' }` to import them as shared modules: ```ts // Import and use directly import { animate } from 'motion-dom' with { runtime: 'shared' } const onScroll = () => { 'main thread' // Can be called directly in a main thread function animate(element, { opacity: 0 }) } ``` To facilitate reuse or avoid the [limitations](#limitations) below (e.g., losing the shared characteristic after assignment), we recommend **wrapping** the import in a main thread function: ```ts // src/utils/motion.ts // 1. Import the original function as shared import { animate as _animate } from 'motion-dom' with { runtime: 'shared' } // 2. Export a new main thread function for encapsulation export function animate(...args) { 'main thread' return _animate(...args) } ``` In this way, `animate` becomes a standard main thread function that can be freely used in any main thread function, no longer restricted by static analysis: ```vue ``` ### Limitations Only identifiers directly imported via `import` are recognized as "shared". Assigning to a new variable will cause it to lose the shared characteristic: ```ts import { func } from './utils' with { runtime: 'shared' } const anotherFunc = func // [!code error] const onTap = () => { 'main thread' anotherFunc() // ❌ Cannot be analyzed at compile time, call fails } ``` ### State Isolation **Variables and state in a shared module are completely isolated between the two threads, each possessing independent instances.** This means that if you modify a variable in a shared module on the main thread, the background thread cannot perceive it, and vice versa. Shared modules solve the "code sharing" problem, not the "state sharing" problem. --- url: /guide/pinia.md --- # Using Pinia [Pinia](https://pinia.vuejs.org/) is the official state management library for Vue. It provides a type-safe, extensible, and modular store with an intuitive API built on the Composition API. See also: [Vue.js — State Management](https://vuejs.org/guide/scaling-up/state-management.html#pinia) ## Installation ```bash npm install pinia ``` ## Setup Register Pinia with your vue-lynx app: ```ts title="src/index.ts" import { createApp } from 'vue-lynx'; import { createPinia } from 'pinia'; import App from './App.vue'; const app = createApp(App); app.use(createPinia()); app.mount(); ``` ## Defining a Store Use `defineStore` with the [setup syntax](https://pinia.vuejs.org/core-concepts/#setup-stores) to create a store: ```ts title="src/stores/counter.ts" import { ref, computed } from 'vue'; import { defineStore } from 'pinia'; export const useCounterStore = defineStore('counter', () => { const count = ref(0); const doubleCount = computed(() => count.value * 2); function increment() { count.value++; } return { count, doubleCount, increment }; }); ``` ## Using the Store in a Component ```vue title="src/CounterSection.vue" ``` See [Pinia — Core Concepts](https://pinia.vuejs.org/core-concepts/) for additional resources. --- url: /guide/quick-start.md --- import { PackageManagerTabs, Steps } from '@theme'; # Quick Start Welcome to Vue Lynx! Let's create a Vue Lynx project and start developing. ## System Requirements * [Node.js 18](https://nodejs.org/en) or later. * Requires Node.js 18.19 when using TypeScript as configuration. ## Start Developing The quickest way to get started with Vue Lynx is to set up only the frontend project and preview with **Lynx Explorer** (for native) or **Lynx for Web** (for browser). ### Create a new Vue Lynx project We use [`create-vue-lynx`](https://npmjs.org/package/create-vue-lynx) to scaffold a new project: After completing the prompts, `create-vue-lynx` will create a folder with your project name containing a starter app. ### Run the dev server 1. Navigate to the created project: ```bash cd ``` 2. Install dependencies: 3. Start the development server: You should see output like this: ``` Rspeedy v0.13.5 ➜ Web http://localhost:3000/main.web.bundle ➜ Web Preview http://localhost:3000/__web_preview?casename=main.web.bundle ➜ Lynx http://localhost:3000/main.lynx.bundle ``` The dev server produces three URLs: * **Web Preview** — Open this in your browser for a quick preview of your app. * **Lynx** — The bundle URL for native rendering via Lynx Explorer (see next step). * **Web** — The raw web bundle (useful for integration into web containers). ### Preview on the Web Open the **Web Preview** URL in your browser. You'll see a live preview of your app rendered on the Web, updating automatically as you edit code. ### Preview on a native device To see your app rendered natively, use **Lynx Explorer**. Scan the QR code shown in the terminal, or copy the **Lynx** bundle URL and paste it into "Enter Card URL" in Lynx Explorer. #### iOS Simulator 1. **Install Xcode** from the [Mac App Store](https://apps.apple.com/us/app/xcode/id497799835). 2. **Download LynxExplorer** For Apple Silicon (M1/M2/M3), download [`LynxExplorer-arm64.app.tar.gz`](https://github.com/lynx-family/lynx/releases/latest/download/LynxExplorer-arm64.app.tar.gz), then extract: ```bash mkdir -p LynxExplorer-arm64.app/ tar -zxf LynxExplorer-arm64.app.tar.gz -C LynxExplorer-arm64.app/ ``` For Intel Mac, download [`LynxExplorer-x86_64.app.tar.gz`](https://github.com/lynx-family/lynx/releases/latest/download/LynxExplorer-x86_64.app.tar.gz), then extract: ```bash mkdir -p LynxExplorer-x86_64.app/ tar -zxf LynxExplorer-x86_64.app.tar.gz -C LynxExplorer-x86_64.app/ ``` 3. **Install on Simulator** — Open Xcode, choose **Open Developer Tool** > **Simulator**, then drag the `.app` folder into it. #### Android Download the pre-built APK from [GitHub Releases](https://github.com/lynx-family/lynx/releases/latest/download/LynxExplorer-noasan-release.apk) and install on your device. :::info Community downloads Lynx Explorer is also available on the [App Store](https://apps.apple.com/us/app/lynx-go-dev-explorer/id6743227790) and [Play Store](https://play.google.com/store/apps/details?id=com.funcs.io.lynx.go), published by community contributors. These versions are not maintained by the Lynx team. ::: :::details Build from source If the pre-built binaries don't work for your environment, you can build Lynx Explorer from source: * [Build for iOS](https://github.com/lynx-family/lynx/tree/develop/explorer/darwin/ios) * [Build for Android](https://github.com/lynx-family/lynx/tree/develop/explorer/android) ::: ### Debugging Download the [Lynx DevTool](https://github.com/lynx-family/lynx-devtool/releases) desktop application. Connect your device via USB and start debugging. See the [Lynx debugging guide](https://lynxjs.org/guide/devtool/panels) to learn more. ## Going to Production The Vue Lynx project you created is a **frontend** project — it produces JavaScript bundles that a Lynx-enabled native app loads and renders. Lynx Explorer is a pre-built native app for development, but for production you'll need your own native app. ### Start with Sparkling (Experimental) [Sparkling](https://tiktok.github.io/sparkling) is TikTok's open-source infrastructure for building apps with Lynx. It provides CLI tools to scaffold a native app project with scheme-driven navigation in minutes. Currently **experimental** and supports **iOS and Android** only. ### Integrate into an existing app For the most flexibility, you can integrate Lynx directly into your existing app. This approach supports **iOS, Android, Harmony, Desktop, and Web**, giving you full control over how Lynx is embedded within your application. Follow the [Lynx integration guide](https://lynxjs.org/guide/start/integrate-with-existing-apps) to get started. ## Next Steps * [What is Vue Lynx?](/guide/introduction.md) — Understand the architecture and key differences from Vue for Web * [Tutorial: Product Gallery](/guide/tutorial-gallery.md) — Build a waterfall gallery with interactivity * [Tutorial: Product Detail](/guide/tutorial-swiper.md) — Build a touch-swipeable image carousel --- url: /guide/routing.md --- import { PackageManagerTabs } from '@theme'; # Using Vue Router Vue Lynx applications can use [Vue Router](https://router.vuejs.org/), the official routing library for Vue, to manage navigation between views. However, since Lynx has no browser `window.location` or History API, you must use [`createMemoryHistory()`](https://router.vuejs.org/api/index.html#creatememoryhistory) instead of `createWebHistory()` — similar to how React Router provides a `MemoryRouter` or TanStack Router provides a memory history for non-browser environments. ## Installing Dependencies ## Creating the Router Use `createMemoryHistory()` to create a history instance that keeps routing state entirely in-process, with no dependency on browser APIs: ```ts title="src/router.ts" import { createRouter, createMemoryHistory } from 'vue-router'; import Home from './views/Home.vue'; import About from './views/About.vue'; const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About }, { path: '/users', name: 'users', component: UserList }, { path: '/users/:id', name: 'user-detail', component: UserDetail }, ], }); export default router; ``` Then install the router plugin and mount the app: ```ts title="src/index.ts" import { createApp } from 'vue-lynx'; import router from './router'; import App from './App.vue'; const app = createApp(App); app.use(router); app.mount(); ``` ## Using `` [``](https://router.vuejs.org/api/index.html#RouterView) renders the component matched by the current route. It works in Lynx without any modifications: ```vue title="src/App.vue" ``` ## Navigating Without `` Tags In a browser, [``](https://router.vuejs.org/api/index.html#RouterLink) renders an `` tag by default. Since Lynx has no `` element, you have two options: ### Option 1: RouterLink with `custom` Slot Use RouterLink's [`custom`](https://router.vuejs.org/api/index.html#RouterLink-Props) prop with the scoped slot API to render Lynx-native elements while retaining `isActive` state: ```vue title="src/NavLink.vue" ``` ### Option 2: Programmatic Navigation Use the [`useRouter()`](https://router.vuejs.org/api/index.html#useRouter) composable for programmatic navigation: ```vue title="src/views/UserList.vue" ``` You can also use `router.back()` and `router.replace()` as you normally would. ## Dynamic Route Params Access dynamic route parameters via [`useRoute()`](https://router.vuejs.org/api/index.html#useRoute): ```vue title="src/views/UserDetail.vue" ``` ## Why Memory History? | History mode | Requires browser APIs | Works in Lynx | | --- | --- | --- | | `createWebHistory()` | Yes (`window.location`, History API) | No | | `createWebHashHistory()` | Yes (`window.location`) | No | | `createMemoryHistory()` | No | **Yes** | `createMemoryHistory()` is designed for environments without a browser — SSR, testing, and native runtimes like Lynx. The routing state is stored in a simple in-memory array, so features like `router.push()`, `router.back()`, and dynamic params all work as expected. --- url: /guide/scroll-view-vs-list.md --- # scroll-view vs list Lynx does not scroll arbitrary nodes the way the Web does. When content overflows a viewport you pick an explicit scroll container — almost always [``](https://lynxjs.org/api/elements/built-in/scroll-view) or [``](https://lynxjs.org/api/elements/built-in/list). This guide explains how Lynx documents the difference, how that differs from Web Vue, and the Vue Lynx patterns that fall out of those choices. For the platform reference, see [Managing Scrolling](https://lynxjs.org/guide/ui/scrolling) on lynxjs.org. ## Lynx vs Web On the Web, almost any element can become a scrollport: ```html
``` On Lynx, a plain `` **does not** gain scrolling from `overflow: scroll` / `overflow: auto`. Only dedicated containers such as `` and `` scroll. That is the first design shift when you move a Vue app from DOM to Lynx: scrolling becomes a **structural** decision in the template, not a CSS property you sprinkle later. | | Web (Vue) | Lynx (Vue Lynx) | |---|---|---| | How scrolling starts | `overflow` on any node | Dedicated `` / `` | | Virtualization | Optional library (`vue-virtual-scroller`, Virtua, …) | Built into `` | | Large feeds | You own windowing + recycled DOM | Native recycling + lazy create | | Complex grids | CSS Grid / masonry libs | `` | Vue Lynx keeps Composition API, `v-for`, and SFCs — but your scroll trees look different from a typical Nuxt / Vite SPA. ## What lynxjs.org says Lynx's scrolling guide draws a clean line between the two containers: 1. **`` for basic scrolling** — a fixed viewport; when children exceed it, set `scroll-orientation` to `vertical` or `horizontal`. 2. **`` for large / infinite data** — on-demand creation of visible items only. 3. **`` for complex layouts** — `scroll-view` is linear only; `list` adds `single`, `flow`, and `waterfall`. The [`` API](https://lynxjs.org/api/elements/built-in/scroll-view) also warns: * Every child is created up front (can hurt first paint). * There is **no reuse**; too much content can exhaust memory. * Prefer `` once content exceeds roughly **three screens**, or fake recycling with exposure events. Think of `` as “a short page that happens to scroll,” and `` as “a recycling feed.” ## Choose with a table | Question | Prefer `` | Prefer `` | |---|---|---| | How much content? | A few screens or less | Many screens / unbounded | | Layout | Linear stack | Single / grid (`flow`) / waterfall | | Item shape | Mixed sections, sticky chrome | Homogeneous (or mostly) cells | | Memory | Eager, all children live | Recycled + lazy | | Infinite load | Possible, but risky | Native `@scrolltolower` | Rule of thumb from Lynx: **under ~3 screens → `scroll-view`; beyond that → `list`.** ## `` in Vue Use ordinary Vue children — `v-for`, nested SFCs, sticky siblings. Nothing special is required beyond wrapping them in ``. ```vue {{ card.title }} ``` Good fits: settings screens, forms, article bodies, short result pages (see also the [Vue Query](/guide/data-fetching.md) and [TodoMVC](/guide/todomvc.md) examples). :::tip Nested layout tip Direct children of `` only support linear / sticky layout. For richer CSS inside the scrollport, wrap content in a single child `` and style that subtree — as recommended in the [scroll-view docs](https://lynxjs.org/api/elements/built-in/scroll-view). ::: ## `` in Vue `` expects **`` children**. Each item needs: * Vue's `:key` — reconciliation identity for the VNode tree * Lynx's `:item-key` — identity for the native recycler (keep them equal) Missing or colliding keys is a common cause of blank / wrong cells. ```vue ``` `estimated-main-axis-size-px` helps the engine size the scrollbar and jump before a cell has been measured — the [gallery tutorial](/guide/tutorial-gallery.md) relies on the same hint for waterfall images. ### Waterfall / flow layouts This is the other half of the lynxjs.org distinction: `scroll-view` cannot do multi-column masonry; `list` can. ```vue ``` Each `list-item` can host a full Vue SFC — the gallery tutorial's `LikeImageCard` is the same pattern at product scale. ## Vue design patterns that fall out of this Moving from Web Vue to Vue Lynx does not change reactivity — it changes **where** you put scrolling and **who** owns recycling. ### 1. Explicit containers in the template Stop reaching for `overflow: auto` on a root ``. Decide up front: ```vue ``` That structural choice is the Lynx equivalent of picking a layout library on the Web. ### 2. Dual identity: `:key` + `:item-key` Web `v-for` only needs `:key`. Lynx lists need both. Treat them as one stable business id: ```vue ``` The [Elk](/guide/elk.md) port follows exactly this contract for Mastodon statuses. ### 3. Composable owns pages; `` owns recycling On the Web, infinite feeds usually mean: `useInfiniteQuery` / custom composable **+** a virtualizer **+** an intersection sentinel. On Lynx, drop the virtualizer. A composable appends to a `ref` array; `@scrolltolower` (and `lower-threshold-item-count`) asks for the next page. Native recycling keeps memory flat. ```ts // shared/useInfiniteFeed.ts export function useInfiniteFeed(pageSize = 20) { const items = ref(makeCards(pageSize)) const loading = ref(false) // loadMore() appends another page… return { items, loading, loadMore } } ``` ```vue ``` This is the same shape as Elk's `TimelinePaginator`: masto.js pagination in a composable, `` for the viewport. :::tip List diffs Vue Lynx's Main Thread list adapter flushes `insertAction` / `removeAction` / `updateAction` by diffing the last-flushed snapshot against the live `listItems` array (LIS move detection, same idea as ReactLynx's remove+insert moves). Covers append, prepend, mid-list insert, remove, same-list reorder, and platform-info updates. Still open: framework-side cell recycling and refreshing `__UpdateListCallbacks` each flush — [#302](https://github.com/Huxpro/vue-lynx/issues/302), [#303](https://github.com/Huxpro/vue-lynx/issues/303). ::: ### 4. Prefer `list` over third-party virtual scrollers Libraries that measure DOM nodes (`getBoundingClientRect`, absolute positioning of windows) do not map cleanly onto Lynx's dual-thread + native elements model. Prefer the built-in recycler unless you have a rare layout `` cannot express. ### 5. Keep cells as Vue components Recycling is a **native** concern; composition stays a **Vue** concern. Put presentational SFCs inside `list-item` — do not flatten everything into one mega-template just because the parent is a list. ```vue ``` ## Mutation demos | Entry | Mutation | Status | |---|---|---| | `ListPrepend` | unshift vs append | Fixed — INSERT respects anchor | | `ListReorder` | Yellow→top / reverse | Fixed — same-list move = detach + insert | | `ListRemove` | splice + Reset same keys | Fixed — `removeAction` (was 2202) | | `ListFilter` | even-only toggle | OK | ### Prepend Tap **Prepend** once. The TOP list cell must turn red (`NEW …`). **Append** lands at the bottom. ### Reorder Four full-bleed colors. The top strip is plain ``s (Vue truth). The `` below must show the **same order** after **Yellow → top** or **Reverse**. ### Remove + filter **Remove**: delete rows, then **Reset same keys** — must not toast duplicated item-key (2202). Filter toggle is a lighter regression tap. ### Still open * Framework-side cell recycling (`enqueueComponent` / recycle pool) — [#302](https://github.com/Huxpro/vue-lynx/issues/302) * Refreshing `__UpdateListCallbacks` on every list flush — [#303](https://github.com/Huxpro/vue-lynx/issues/303) Automated coverage: `packages/testing-library` → **native list element · mutations**. ## Decision checklist 1. Is the page mostly one linear document under ~3 screens? → **``** 2. Are you rendering dozens / hundreds of similar rows? → **``** 3. Do you need waterfall or multi-column flow? → **``** 4. Will the dataset grow without bound? → **`` + composable + `@scrolltolower`** 5. Did you set matching `:key` / `:item-key`? → required for correct recycling 6. Prepend / reorder / remove? → supported via `update-list-info` diffs ## See also * [Managing Scrolling (Lynx)](https://lynxjs.org/guide/ui/scrolling) * [`` API](https://lynxjs.org/api/elements/built-in/scroll-view) * [`` API](https://lynxjs.org/api/elements/built-in/list) * [Tutorial: Product Gallery](/guide/tutorial-gallery.md) — waterfall `` in a full app * [Elk (Mastodon Client)](/guide/elk.md) — replacing Virtua with native `` * [Main Thread Script](/guide/main-thread-script.md) — scroll-linked worklets on `` / `` --- url: /guide/showcase-scale.md --- # Showcase scale compare Internal check: does live web at `fit` @ 360×804 match the density of the original homepage showcase videos (1080×2412 = 360×804 CSS-px @3×)? Left = recording (`object-fit: cover`). Right = `` into the same bezel. Typography / card sizes should line up; if the live side looks much larger, the design canvas is too small (or we fell back to responsive layout at bezel CSS pixels). --- url: /guide/tailwindcss.md --- # Using Tailwind CSS [Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework. Combined with Vue Lynx, it lets you build native Lynx UIs using the same utility classes you already know from the web. This guide walks through integrating Tailwind CSS with Vue Lynx, from basic setup to a design-token system with runtime theme switching — inspired by [shadcn/ui](https://ui.shadcn.com/). ## Setup ### 1. Install dependencies ```bash pnpm add -D tailwindcss@3 @lynx-js/tailwind-preset rsbuild-plugin-tailwindcss ``` :::tip Tailwind v3 Required `@lynx-js/tailwind-preset` and `rsbuild-plugin-tailwindcss` require **Tailwind CSS v3**. Tailwind v4 uses a completely different architecture and is not yet supported. Make sure you do **not** have `@tailwindcss/postcss` or `@tailwindcss/vite` installed — those are v4-only packages that conflict with the v3 setup. ::: * **`@lynx-js/tailwind-preset`** — A Tailwind preset that replaces core plugins with Lynx-compatible equivalents. See the [official Rspeedy docs](https://lynxjs.org/rspeedy/styling.html#using-tailwind-css) for details. * **`rsbuild-plugin-tailwindcss`** — Rsbuild integration for Tailwind CSS. ### 2. Configure Tailwind ```ts title="tailwind.config.ts" import type { Config } from 'tailwindcss'; import preset from '@lynx-js/tailwind-preset'; const config: Config = { content: ['./src/**/*.{vue,js,ts}'], presets: [preset], }; export default config; ``` ### 3. Configure PostCSS ```js title="postcss.config.js" export default { plugins: { tailwindcss: {}, }, }; ``` ### 4. Add the Rsbuild plugin ```ts title="lynx.config.ts" import { defineConfig } from '@lynx-js/rspeedy'; import { pluginTailwindCSS } from 'rsbuild-plugin-tailwindcss'; import { pluginVueLynx } from 'vue-lynx/plugin'; export default defineConfig({ plugins: [ pluginVueLynx(), pluginTailwindCSS({ config: 'tailwind.config.ts', exclude: [/[\\/]node_modules[\\/]/], }), ], }); ``` ### 5. Import Tailwind in your CSS ```css title="src/App.css" @tailwind base; @tailwind utilities; ``` That's it! You can now use Tailwind utility classes on Lynx elements: ```vue title="src/App.vue" ``` ## Design Tokens with CSS Variables A common pattern in modern component libraries (shadcn/ui, Radix Themes, Nuxt UI) is to define a **design language** as CSS custom properties, then reference them from Tailwind. This gives you a single source of truth for colors that can be swapped at runtime. ### Define your tokens ```css title="src/App.css" @tailwind base; @tailwind utilities; :root { --color-background: rgba(9, 9, 11, 1); --color-card: rgba(24, 24, 27, 1); --color-card-foreground: rgba(250, 250, 250, 1); --color-primary: rgba(255, 100, 72, 1); --color-primary-foreground: rgba(255, 255, 255, 1); --color-border: rgba(63, 63, 70, 1); } ``` ### Wire them into Tailwind ```ts title="tailwind.config.ts" const config: Config = { // ... theme: { extend: { colors: { background: 'var(--color-background)', card: { DEFAULT: 'var(--color-card)', foreground: 'var(--color-card-foreground)', }, primary: { DEFAULT: 'var(--color-primary)', foreground: 'var(--color-primary-foreground)', }, border: 'var(--color-border)', }, }, }, }; ``` Now `bg-primary`, `text-card-foreground`, `border-border` all resolve through your CSS variables. The `:root` values act as the default theme. ### Required Lynx flags For CSS variables to work on Lynx Native, two engine flags must be enabled: * **`enableCSSInheritance`** — Enables CSS cascade from parent elements to children, so variables defined on a parent (or `:root`) are visible to descendants. See the [Lynx CSS Variable docs](https://lynxjs.org/api/css/properties/css-variable.html). * **`enableCSSInlineVariables`** — Enables `--*` properties in inline styles, so `:style` bindings can set CSS variables at runtime. ```ts title="lynx.config.ts" pluginVueLynx({ enableCSSInheritance: true, enableCSSInlineVariables: true, }) ``` > These flags are **only needed when using CSS variables**. If your Tailwind config uses static color values (e.g. `primary: '#3b82f6'`), no additional flags are required. ## Runtime Theme Switching With design tokens wired through CSS variables, switching themes is just a matter of overriding the variable values. ### Approach A: Inline style (recommended) The most web-familiar approach — set CSS variables via Vue's `:style` binding on a root element. This is how shadcn/ui and Radix Themes handle theming on the web. Requires both `enableCSSInheritance` and `enableCSSInlineVariables`. ```vue ``` ### Approach B: Element `setProperty` API A Lynx-native alternative that updates CSS variables directly on an element. Does **not** require `enableCSSInlineVariables` — only `enableCSSInheritance`. ```vue ``` See the [Lynx CSS Variable API](https://lynxjs.org/api/css/properties/css-variable.html) for full details on `setProperty`. ## Troubleshooting ### New Tailwind classes don't appear on HMR **Symptom:** You change e.g. `bg-slate-800` to `bg-red-500`, save, but the emulator doesn't update. Switching *back* to a previously-used class works, but new classes don't show up until you restart the dev server. **Cause:** Tailwind v3's JIT compiler generates CSS only for the utility classes it finds in your source files. When a file changes, JIT must re-scan it and generate any newly-referenced classes. If the PostCSS pipeline is misconfigured, this re-scan doesn't trigger on HMR, so only classes that were already in the initial bundle work. The most common cause is **mixing Tailwind v3 and v4 packages**: | Package | Version | What it is | |---------|---------|------------| | `tailwindcss` | 3.x | Tailwind v3 core (also the PostCSS plugin) | | `@tailwindcss/postcss` | 4.x | Tailwind **v4** PostCSS plugin -- **incompatible with v3** | These two cannot coexist. The Lynx ecosystem (`@lynx-js/tailwind-preset`, `rsbuild-plugin-tailwindcss`) requires **Tailwind v3**. **Fix:** Remove the v4 package and any extra PostCSS dependencies: ```bash pnpm remove @tailwindcss/postcss autoprefixer ``` Then follow the [setup steps above](#1-install-dependencies). Make sure your `postcss.config.js` uses `tailwindcss` (the v3 plugin), **not** `@tailwindcss/postcss`. ### `content` path doesn't match your source files If Tailwind classes work in some files but not others, check that the `content` array in `tailwind.config.ts` includes all relevant paths: ```ts content: ['./src/**/*.{vue,js,ts}'], ``` Adjust the glob if your source files live elsewhere (e.g. `pages/`, `components/`). --- url: /guide/testing-library.md --- import { PackageManagerTabs } from '@theme'; # VueLynx Testing Library The `vue-lynx-testing-library` package offers APIs like `render`, `fireEvent`, and `getByText` for testing Vue Lynx components, similar to [Vue Test Utils](https://test-utils.vuejs.org/) and [React Testing Library](https://testing-library.com/), with the dual-threaded architecture abstracted through [`@lynx-js/testing-environment`](https://www.npmjs.com/package/@lynx-js/testing-environment). ## Setup ### From create-vue-lynx Using `create-vue-lynx` sets up VueLynx Testing Library automatically, providing pre-configured testing support. ### Adding to an existing project Install the required dependencies: For Vitest configuration, create a `vitest.config.ts` that aliases `vue-lynx` subpaths and registers the setup file: ```ts title="vitest.config.ts" import { defineConfig } from 'vitest/config'; import path from 'node:path'; export default defineConfig({ test: { environment: 'jsdom', globals: true, setupFiles: [path.resolve(__dirname, 'test/setup.ts')], include: ['test/**/*.test.ts'], alias: [ { find: 'vue-lynx/entry-background', replacement: path.resolve( __dirname, 'node_modules/vue-lynx/runtime/dist/entry-background.js', ), }, { find: 'vue-lynx/main-thread', replacement: path.resolve( __dirname, 'node_modules/vue-lynx/main-thread/dist/entry-main.js', ), }, { find: 'vue-lynx/internal/ops', replacement: path.resolve( __dirname, 'node_modules/vue-lynx/internal/dist/ops.js', ), }, { find: /^vue-lynx$/, replacement: path.resolve( __dirname, 'node_modules/vue-lynx/runtime/dist/index.js', ), }, ], }, }); ``` Create a setup file that initializes the dual-thread testing environment. This runs before any test module is imported, so all Lynx globals are in place when Vue's runtime loads: ```ts title="test/setup.ts" import { JSDOM } from 'jsdom'; import { LynxTestingEnv } from '@lynx-js/testing-environment'; // Create the testing environment const jsdom = new JSDOM(''); const lynxTestingEnv = new LynxTestingEnv(jsdom); (globalThis as any).lynxTestingEnv = lynxTestingEnv; // Wire Main Thread globals lynxTestingEnv.switchToMainThread(); if (typeof (globalThis as any).registerWorkletInternal === 'undefined') { (globalThis as any).registerWorkletInternal = () => {}; } await import('vue-lynx/main-thread'); const mainThreadFns = { renderPage: (globalThis as any).renderPage, vuePatchUpdate: (globalThis as any).vuePatchUpdate, processData: (globalThis as any).processData, updatePage: (globalThis as any).updatePage, updateGlobalProps: (globalThis as any).updateGlobalProps, }; const mtGlobal = lynxTestingEnv.mainThread.globalThis as any; Object.assign(mtGlobal, mainThreadFns); // Wire Background Thread globals lynxTestingEnv.switchToBackgroundThread(); await import('vue-lynx/entry-background'); const publishEventFn = (globalThis as any).publishEvent; const bgGlobal = lynxTestingEnv.backgroundThread.globalThis as any; bgGlobal.publishEvent = publishEventFn; // Re-wire globals after env resets between tests (globalThis as any).onSwitchedToMainThread = () => { Object.assign(globalThis, mainThreadFns); }; (globalThis as any).onSwitchedToBackgroundThread = () => { if ((globalThis as any).lynxCoreInject?.tt) { (globalThis as any).lynxCoreInject.tt.publishEvent = publishEventFn; } (globalThis as any).publishEvent = publishEventFn; }; ``` ## Examples ### Quick Start Follow the **Arrange-Act-Assert** pattern: prepare test data, perform operations, then assert results: ```ts import { expect, it, vi } from 'vitest'; import { h, defineComponent } from 'vue-lynx'; import { render, fireEvent } from 'vue-lynx-testing-library'; it('basic', async () => { const onClick = vi.fn(); const Button = defineComponent({ props: { onClick: Function }, setup(props, { slots }) { return () => h('view', { bindtap: props.onClick }, slots.default?.()); }, }); // ARRANGE const { container } = render( defineComponent({ setup() { return () => h(Button, { onClick }, () => [ h('text', null, 'Click me'), ]); }, }), ); expect(onClick).not.toHaveBeenCalled(); // ACT fireEvent.tap(container.querySelector('view')!); // ASSERT expect(onClick).toBeCalledTimes(1); expect(container.querySelector('text')!.textContent).toBe('Click me'); }); ``` VueLynx Testing Library uses JSDOM to implement [Element PAPI](https://lynxjs.org/guide/spec.html), so you can query rendered elements with `container.querySelector()` and assert their `textContent`. :::tip When passing slot content to a component with `h()`, wrap children in a function: `h(Comp, props, () => [children])`. This follows Vue 3's recommended function slot pattern. ::: ### Basic rendering The `render` method returns a `RenderResult` object with a `container` field containing the rendered result: ```ts import { expect, it } from 'vitest'; import { h, defineComponent } from 'vue-lynx'; import { render } from 'vue-lynx-testing-library'; it('basic render', () => { const Comp = defineComponent({ render() { return h('view', { id: 'inner', style: { backgroundColor: 'yellow' } }); }, }); const { container } = render(Comp); expect(container.querySelector('#inner')).not.toBeNull(); expect(container.querySelector('view')).not.toBeNull(); }); ``` You can also use the `@testing-library/dom` queries bound to the container: ```ts const { getByText } = render(Comp); expect(getByText('Hello')).not.toBeNull(); ``` ### Firing events When using `fireEvent`, the event type is determined by the handler property name on the element. The format follows `eventType:eventName` (e.g., `catchEvent:tap` triggers a catch-type tap event). Event handler properties determine the event type: | Event Type | `eventType` | Binding Example | Triggering Example | |---|---|---|---| | `bind` | `bindEvent` | `bindtap` | `fireEvent.tap(el)` | | `catch` | `catchEvent` | `catchtap` | `fireEvent.tap(el, { eventType: 'catchEvent' })` | | `capture-bind` | `capture-bind` | `capture-bindtap` | `fireEvent.tap(el, { eventType: 'capture-bind' })` | | `capture-catch` | `capture-catch` | `capture-catchtap` | `fireEvent.tap(el, { eventType: 'capture-catch' })` | You can construct Event objects directly or use the named helpers for automatic construction: ```ts import { h, defineComponent } from 'vue-lynx'; import { render, fireEvent } from 'vue-lynx-testing-library'; import { vi, expect, it } from 'vitest'; it('fireEvent', async () => { const handler = vi.fn(); const Comp = defineComponent({ setup() { return () => h('view', null, [h('text', { catchtap: handler })]); }, }); const { container } = render(Comp); const textEl = container.querySelector('text')!; expect(handler).toHaveBeenCalledTimes(0); // Method 1: Use the named helper fireEvent.tap(textEl, { eventType: 'catchEvent', key: 'value', }); expect(handler).toHaveBeenCalledTimes(1); // Method 2: Construct the Event object yourself const event = new Event('catchEvent:tap'); Object.assign(event, { eventType: 'catchEvent', eventName: 'tap', key: 'value2', }); fireEvent(textEl, event); expect(handler).toHaveBeenCalledTimes(2); }); ``` The following event helpers are available: | Helper | Event Name | |---|---| | `fireEvent.tap(el)` | `tap` | | `fireEvent.longtap(el)` | `longtap` | | `fireEvent.longpress(el)` | `longpress` | | `fireEvent.touchstart(el)` | `touchstart` | | `fireEvent.touchmove(el)` | `touchmove` | | `fireEvent.touchend(el)` | `touchend` | | `fireEvent.touchcancel(el)` | `touchcancel` | | `fireEvent.scroll(el)` | `scroll` | | `fireEvent.scrollend(el)` | `scrollend` | | `fireEvent.focus(el)` | `focus` | | `fireEvent.blur(el)` | `blur` | | `fireEvent.layoutchange(el)` | `layoutchange` | | `fireEvent.transitionend(el)` | `transitionend` | | `fireEvent.animationend(el)` | `animationend` | ### Testing reactivity After changing reactive state, use `await nextTick(); await nextTick()` to wait for Vue's scheduler to flush and for the ops to be applied on the main thread. The convenience helper `waitForUpdate()` wraps both calls: ```ts import { h, defineComponent, ref } from 'vue-lynx'; import { render, waitForUpdate } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('updates text when ref changes', async () => { const count = ref(0); const Comp = defineComponent({ setup() { return () => h('text', null, `Count: ${count.value}`); }, }); const { container } = render(Comp); expect(container.querySelector('text')!.textContent).toBe('Count: 0'); count.value = 42; await waitForUpdate(); expect(container.querySelector('text')!.textContent).toBe('Count: 42'); }); ``` ### Testing template refs Vue Lynx sets a `vue-ref-{id}` attribute on elements, which can be used to verify ref assignment. The `ShadowElement` (background thread representation) provides `NodesRef` methods like `invoke`, `setNativeProps`, and `animate`: ```ts import { h, defineComponent, ShadowElement } from 'vue-lynx'; import { render } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('element ref', () => { const Comp = defineComponent({ render() { return h('view', null, [h('text', null, 'hello')]); }, }); const { container } = render(Comp); const view = container.querySelector('view')!; // Vue Lynx sets vue-ref-{id} attribute for elements expect(view.hasAttribute('vue-ref-2')).toBe(true); }); it('ShadowElement has NodesRef methods', () => { const el = new ShadowElement('view'); expect(typeof el.invoke).toBe('function'); expect(typeof el.setNativeProps).toBe('function'); expect(typeof el.animate).toBe('function'); }); ``` ### Querying page elements The `render` method returns `@testing-library/dom` query methods bound to the container, such as `getByText`: ```ts import { h, defineComponent, ref } from 'vue-lynx'; import { render, waitForUpdate } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('queries rendered elements', async () => { const loaded = ref(false); const Comp = defineComponent({ setup() { return () => loaded.value ? h('text', { id: 'message' }, 'Hello World') : h('text', null, 'Loading...'); }, }); const { container, getByText } = render(Comp); expect(getByText('Loading...')).not.toBeNull(); loaded.value = true; await waitForUpdate(); expect(container.querySelector('#message')!.textContent).toBe('Hello World'); }); ``` ### Rerendering The `render` method returns an object with a `rerender` method for testing different component states: ```ts import { h, defineComponent } from 'vue-lynx'; import { render } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('rerender will re-render your component', () => { const Greeting = defineComponent({ props: { message: String }, render() { return h('text', null, this.message); }, }); const { container, rerender } = render(Greeting, { message: 'hi' }); expect(container.querySelector('text')!.textContent).toBe('hi'); { const { container } = rerender(Greeting, { message: 'hey' }); expect(container.querySelector('text')!.textContent).toBe('hey'); } }); ``` ### Testing list The `list` element renders `list-item` children. Items are described declaratively and the native list handles lazy loading: ```ts import { h, defineComponent, ref } from 'vue-lynx'; import { render, waitForUpdate } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('list', async () => { const items = ref([0, 1, 2]); const Comp = defineComponent({ setup() { return () => h( 'list', null, items.value.map((item) => h('list-item', { key: item, 'item-key': item }, [ h('text', null, `${item}`), ]), ), ); }, }); const { container } = render(Comp); const list = container.querySelector('list'); expect(list).not.toBeNull(); // Add items reactively items.value = [0, 1, 2, 3]; await waitForUpdate(); expect(container.querySelector('list')).not.toBeNull(); }); ``` ### Testing Main Thread Script Main Thread Script testing requires no special configuration. Note that background thread methods cannot be called directly from main thread scripts; place callback functions on `globalThis` for assertion: ```ts import { h, defineComponent } from 'vue-lynx'; import { render } from 'vue-lynx-testing-library'; import { expect, it } from 'vitest'; it('main thread script', () => { const Comp = defineComponent({ setup() { return () => h('view', { 'main-thread-bindtap': { _wkltId: 1, _closure: {}, }, }, [ h('text', null, 'Hello Main Thread Script'), ]); }, }); const { container } = render(Comp); expect(container.querySelector('text')!.textContent).toBe('Hello Main Thread Script'); }); ``` :::info In production, `'main thread'` directive functions are transformed by the build plugin into worklet context objects (*worklet* is the Lynx engine's internal term for a compiled Main Thread Script function). In tests, you can pass the worklet context directly as shown above, or use the SWC transform if your Vitest config includes the worklet loader. ::: ### More usage For additional examples, see the test cases in the [vue-lynx testing-library source code](https://github.com/Huxpro/vue-lynx/tree/main/packages/testing-library/src/__tests__). ## API Reference See the full [vue-lynx/testing-library API Reference](/guide/api/testing-library/index.md) for details on `render`, `fireEvent`, `cleanup`, `waitForUpdate`, and other exports. --- url: /guide/todomvc.md --- # TodoMVC [TodoMVC](https://github.com/tastejs/todomvc/tree/gh-pages/examples/vue) is the canonical benchmark for comparing JavaScript frameworks. We ported it as our first validation target to verify whether we can fully reuse the Vue 3 core and implement a dual-thread rendering pipeline on top of the Custom Renderer API to connect with Lynx's native engine. ## Overview of Changes Most business logic transferred directly — Composition API (`ref`, `computed`, `watch`), component patterns, and template directives work identically. Here's what we changed: | Aspect | Original (Web) | Vue Lynx | |--------|----------------|----------| | **Template elements** | `
`, `
  • `, `