@sky.ui/reactivity
v0.0.2
Published
Vue-style reactivity and in-memory template compiler for lightweight Sky UI apps.
Readme
@sky.ui/reactivity
Vue-style reactivity and an in-memory template compiler for lightweight Sky apps. Expressions are parsed with Acorn and evaluated through a safe AST interpreter — no eval or new Function.
Install
npm install "@sky.ui/reactivity"Quote scoped names (
"@sky.ui/…") — required in PowerShell. See powershell-scoped-packages.md.
Optional peer for Node SSR:
npm install linkedomOptional: install the Sky UI Reactivity VS Code extension for completions, diagnostics, and navigation for sky-if, sky-for, sky-model, and related directives.
import {
createReactivity,
reactive,
ref,
computed,
watch,
effect,
mount,
renderToString,
nextTick,
} from '@sky.ui/reactivity';Table of contents
- API reference
- Template directives
- Server-side rendering (SSR)
- CRUD: Create, Read, Update, Delete
- Expression context and ref unwrapping
- What is supported in expressions
- What is blocked or unsupported
- Common patterns
- Troubleshooting
- Vue 3 comparison: accuracy and improvements
Quick start
1. HTML with template directives:
<div id="app">
<p>Count: {{ count }}</p>
<button onclick="count++">Increment</button>
<ul sky-for="item in items" sky-key="item.id">
<li>{{ item.name }}</li>
</ul>
</div>2. Mount with a setup function (recommended):
const { mount } = createReactivity(() => ({
count: 0,
items: [
{ id: 1, name: "Alpha" },
{ id: 2, name: "Beta" },
],
}));
const { ctx, unmount } = mount("#app");
// ctx.count, ctx.items are reactive; DOM updates when they change.Same behavior in one expression:
const { ctx, unmount } = createReactivity(() => ({
count: 0,
items: [{ id: 1, name: "Alpha" }],
})).mount("#app");3. Or mount with a plain object + optional initial state:
const { mount } = createReactivity({
count: 0,
items: [],
});
const { ctx, unmount } = mount("#app", { count: 10 });
// ctx is merged: initial + setup. Unmount when done: unmount().API reference
createReactivity
Creates a reactivity app that can mount to a DOM target.
createReactivity(setup: (() => Record<string, unknown>) | Record<string, unknown>): {
mount(target: Element | string, initial?: Record<string, unknown>): {
ctx: Record<string, unknown>;
unmount(): void;
};
}setup: Either a function returning an object, or a plain object. All properties are exposed on the template context and are made reactive if they are objects/arrays.- Function setup with
ref/reactive: PrefercreateReactivity(() => { const items = reactive([]); const flag = ref(false); … return { items, flag, … }; })when you need Vue-style primitives (ref) or in-place mutations on objects/arrays (reactive). Returned refs are auto-unwrapped in template expressions; plain literals in the returned object are still wrapped by the engine where applicable. mount(target, initial?): Compiles the DOM tree undertarget(selector string orElement), creates context fromsetup(andinitialmerged in), and returns{ ctx, unmount }.ctx: The reactive context used in templates. Add or change properties here to drive the UI.unmount(): Cleans up effects and event listeners. Call when removing the app from the page.
mount
Low-level mount: you provide the root element and the context object. Use this when you build ctx yourself (e.g. with reactive/ref).
mount(root: Element | DocumentFragment | string, ctx: Record<string, unknown>): Record<string, unknown>root: DOM element, document fragment, or a CSS selector string.ctx: Any object; typically a reactive object or one whose properties are refs/reactive. Template expressions read from and write toctx.- Returns: The same
ctxyou passed in.
Example:
import { mount, reactive } from "@sky.ui/reactivity";
const ctx = reactive({ name: "World" });
mount("#app", ctx);reactive
Makes a plain object or array reactive. Reading and writing properties is tracked; nested objects and arrays are automatically wrapped when accessed.
reactive<T extends object>(obj: T): T- Same object identity is preserved (cached proxy).
- Use for objects and arrays that you mutate in place (e.g.
state.items.push(item)).
Example:
const state = reactive({ name: "", list: [] });
state.name = "Alice"; // triggers updates
state.list.push("x"); // triggers updatesref
Wraps a single value in an object { value: T } that is reactive. In template expressions, refs are auto-unwrapped (you use count, not count.value). In JS, always use .value to read/write.
ref<T>(v: T): { value: T }Example:
const count = ref(0);
count.value++; // in JS
// In template: {{ count }} and onclick="count++" work; the engine sets count.value.shallowRef
Like ref() but the inner value is not deeply reactive. Only .value access is tracked. Use for large structures or external state. After mutating .value in place, call triggerRef(ref) to notify dependents.
shallowRef<T>(v: T): { value: T }
triggerRef(ref: { value: unknown }): voidExample:
const state = shallowRef({ count: 1 });
state.value.count = 2; // does NOT trigger
state.value = { count: 2 }; // triggers
triggerRef(state); // or after in-place mutation, force triggertriggerRef
Forces effects that depend on a shallow ref to run. Call after mutating ref.value in place (e.g. shallowRef.value.foo = 1). See shallowRef.
computed
Creates a reactive computed value. Accepts either a getter (read-only) or { get, set } (writable). The getter runs in an effect; when its dependencies change, the computed updates.
computed<T>(getter: () => T): { value: T }
computed<T>(options: { get: () => T; set: (v: T) => void }): { value: T }Example:
const first = ref("Jane");
const last = ref("Doe");
const fullName = computed(() => `${first.value} ${last.value}`);
// fullName.value is 'Jane Doe'; when first or last change, fullName.value updates.
// Writable computed
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1; },
});
plusOne.value = 1; // count.value is now 0effect
Runs a function immediately and re-runs it whenever any reactive dependency (reactive object/ref/computed) read inside it changes. Used internally by the template compiler.
effect<T>(fn: () => T, opts?: { lazy?: boolean; scheduler?: (run: () => void) => void }): () => Tlazy: Iftrue, the effect does not run until the returned runner is called.scheduler: If provided, when dependencies change the scheduler is called instead of running the effect immediately (default batches via microtask).- Returns: A runner function. Call it to run the effect again; call
runner.stop()to disable.
Example:
const state = reactive({ count: 0 });
effect(() => {
console.log(state.count);
});
state.count = 1; // logs 1watch
Watches one or more reactive sources and runs a callback when they change. Returns a function that stops the watcher.
watch(
source: unknown | (() => unknown) | (unknown | (() => unknown))[],
cb: (newVal, oldVal, onCleanup: (fn: () => void) => void) => void,
opts?: { immediate?: boolean; deep?: boolean; flush?: 'pre' | 'post' | 'sync' }
): () => voidsource: A ref, reactive object, getter function, or array of these for multiple sources.cb(newVal, oldVal, onCleanup): Called when the value(s) change. For multiple sources,newValandoldValare arrays.onCleanup(fn)registers a cleanup that runs before the next run (e.g. cancel a prior request).opts.immediate: Iftrue, run the callback once immediately (witholdValundefined).opts.deep: Iftrue, traverse the source so nested changes trigger the callback.opts.flush:'post'(default, microtask),'pre', or'sync'(run synchronously when deps change).- Returns: A function; call it to stop the watcher.
Example:
const count = ref(0);
const stop = watch(count, (newVal, oldVal) => {
console.log(oldVal, "->", newVal);
});
stop(); // when done
watch([a, b], ([aVal, bVal], [aOld, bOld], onCleanup) => {
const id = fetchSomething();
onCleanup(() => cancel(id));
}, { immediate: true, flush: 'sync' });nextTick
Schedules a callback after the current microtask queue (e.g. after the current batch of reactive updates).
nextTick(fn?: () => void): Promise<void>Example:
ctx.items.push(newItem);
await nextTick();
// DOM updated; safe to measure or focus.toRaw, markRaw, readonly, isReactive, isRef, unref
| API | Description |
|-----|-------------|
| toRaw(proxy) | Returns the original object from a reactive or readonly proxy. Use to read/write without triggering. |
| markRaw(obj) | Marks obj so reactive(obj) returns obj unchanged (opt-out of reactivity). |
| readonly(obj) | Returns a deep read-only proxy. Mutations are no-ops; refs in properties are unwrapped. |
| isReactive(obj) | true if obj is a reactive proxy. |
| isRef(v) | true if v is a ref (including shallowRef/computed). |
| unref(v) | Returns v.value if v is a ref, otherwise v. |
Example:
const raw = toRaw(reactiveObj);
const obj = markRaw({ noReactivity: true });
const ro = readonly(state);
if (isReactive(ro)) { ... }
const x = unref(maybeRef);Template directives
Directives are attributes on HTML elements. The compiler reads them once at mount and then manages the DOM reactively.
Conditionals: sky-if / sky-else-if / sky-else
sky-if="expr"— Renders the element only whenexpris truthy. Same idea asv-if.sky-else-if="expr"— Else-if branch of the previous sky-if/sky-else-if.sky-else— Final else branch (no expression).
Only one branch is in the DOM at a time. Use when you want to truly add/remove nodes.
<div sky-if="mode === 'edit'">Editing...</div>
<div sky-else-if="mode === 'view'">View only</div>
<div sky-else>Unknown mode</div>You can also use v-if / v-else-if / v-else or w-if / w-else-if / w-else as aliases.
Lists: sky-for
Repeats an element for each item in a source. Syntax:
sky-for="item in items"—itemis the loop variable,itemsis the expression (array, object, string, or number).sky-for="(item, index) in items"— Same, plusindexas the second variable.sky-key="expr"(optional) — Stable key per item for reuse of DOM nodes. If omitted, position is used.
Source types:
| Source | Iteration value | Key (when no sky-key) | | -------- | --------------- | --------------------- | | Array | Element | Index | | Object | Property value | Property key | | String | Character | Index | | Number n | 0..n-1 | Index |
In the loop you have:
item(or your alias) — current value$item— same as item$index— current index (0-based)- If source is object:
$key— current property key
Examples:
<!-- Array -->
<ul>
<li sky-for="item in items" sky-key="item.id">{{ item.name }}</li>
</ul>
<!-- With index -->
<ul>
<li sky-for="(item, i) in items">{{ i }}: {{ item }}</li>
</ul>
<!-- Object: value and $key -->
<div sky-for="(value, idx) in record" sky-key="$key">
{{ $key }}: {{ value }}
</div>
<!-- Number: repeat N times -->
<span sky-for="n in 3">{{ n }}</span>
<!-- String: per character -->
<span sky-for="char in name">{{ char }}</span>CRUD in lists: Prefer mutating the same reactive array (push, splice, sort, etc.) so the engine can reuse nodes by key. See CRUD: Arrays.
Visibility: sky-show
Shows or hides the element with display (no DOM remove). Use when the element is often toggled.
<div sky-show="isVisible">Only visible when isVisible is true</div>Raw HTML: sky-html
Sets innerHTML from an expression. Only use with trusted content — there is no built-in sanitization (XSS risk).
<div sky-html="htmlContent"></div>One-shot bindings: sky-once
Runs a reactive binding once, then stops tracking (attribute sky-once or bindReactive(..., { once: true }) internally).
<p sky-once>{{ expensive() }}</p>Two-way binding: sky-model
Binds a form control to a context property (input, textarea, select, checkbox, radio).
<input type="text" sky-model="username" />
<input type="checkbox" sky-model="agreed" />
<select sky-model="choice"><option value="a">A</option></select>Modifiers (dot suffix on the attribute name, combinable):
| Modifier | Effect |
|----------|--------|
| .lazy | Use change instead of input (text fields / textarea) |
| .number | Coerce written value with Number() |
| .trim | trim() string values on write |
<input sky-model.trim="search" />
<input sky-model.number="age" />
<input sky-model.lazy="notes" />
<input sky-model.number.lazy="score" />Custom elements (tag with a hyphen): Vue-style prop + update:* event.
| Binding | Prop | Event |
|---------|------|--------|
| sky-model="x" | modelValue | update:modelValue |
| sky-model:title="x" | title | update:title |
| sky-model:visible-items="x" | visibleItems (camelCase) | update:visibleItems |
<sky-combobox sky-model="selectedId"></sky-combobox>
<sky-editor sky-model:content="body"></sky-editor>The element should emit CustomEvent('update:…', { detail: newValue }). Modifiers (.number, .trim) apply when writing into context. On native <input> / <select>, use plain sky-model without :arg.
- Read: Template expressions like
{{ username }}use the current value. - Write: User input updates the context property. No need to write
oninputyourself for simple binding. - Value must be a single identifier (e.g.
sky-model="username", not an expression).
Element reference: sky-ref
Assigns the DOM element to a context property. Typically used with a ref so you get someRef.value = the element after mount.
<input sky-ref="inputEl" />In setup:
const inputEl = ref(null);
return { inputEl };After mount, inputEl.value is the input element. Useful for focus, measure, or native APIs. The ref is cleared on unmount.
Class and style
:classorsky-bind:class— Expression can be:- String
- Array of strings
- Object: keys are class names, values are booleans (truthy = add class)
The result is merged with the element’s existing class (base class is preserved).
:styleorsky-bind:style— Expression can be a string (cssText) or an object of style properties.
<div :class="{ active: isActive, 'text-bold': bold }">...</div>
<div :class="[base, dynamicClass]">...</div>
<div :style="{ color: color, fontSize: size + 'px' }">...</div>Element properties: sky-bind:prop
The compiler treats :class and :style (and their sky-bind:class / sky-bind:style aliases) as special bindings. Other Vue-style :someProp attributes are not compiled — use sky-bind:prop instead.
sky-bind:propName="expression"— Evaluates the expression in context and assignsnode[propName]on every change (reactive effects).- The segment after
sky-bind:may be camelCase or kebab-case (e.g.sky-bind:visible-items→visibleItemson the element). - On custom elements, when the value is an array or object, the engine tracks nested reads and passes a shallow clone into the property (friendly for Lit-style reactive props).
<div id="app">
<sky-breadcrumb
sky-bind:items="items"
sky-bind:separator="separator"
@route-change="handleRouteChange($event)"
></sky-breadcrumb>
</div>import { createReactivity, reactive, ref } from "@sky.ui/reactivity";
createReactivity(() => {
const items = reactive([
{ title: "Home", route: "/" },
{ title: "Docs", route: "/docs", active: true },
]);
const separator = ref("/");
function handleRouteChange(e) {
const item = e.detail;
if (item?.route) {
window.history.pushState({}, "", item.route);
}
}
return {
items,
separator,
handleRouteChange,
};
}).mount("#app");Use reactive for objects and arrays you mutate in place (e.g. items.push(…)), and ref for primitives or values you replace wholesale (separator.value = ">"). Templates unwrap refs automatically (sky-bind:separator="separator" reads separator.value).
Event handlers
onclick="...",oninput="...", etc. — Value is a statement (can assign, call functions, etc.).@click="...",@input="..."— Same, with Vue-style@prefix; the event name is everything after@(including hyphens, e.g.@route-change).
Modifiers (on @ attributes only, dot-suffix): .prevent, .stop, .self, .once, .capture, .passive, .exact, key filters (@keyup.enter, @keydown.esc), and system keys (.ctrl, .alt, .shift, .meta).
Handlers without await run synchronously (so if / switch / loops update context before the event returns). Handlers that contain await run on the async interpreter path.
Inside the handler you can use:
$event— The native event object.$el— The element the handler is attached to.- Any context property (refs auto-unwrapped for read/assign, e.g.
count++).
<button onclick="count++">Increment</button>
<button @click.prevent="submit()">Submit</button>
<input @keydown.enter="onEnter()" />
<button @click="await load()">Load</button>Server-side rendering (SSR)
renderToString(markup, ctx, options?) compiles directives and {{ }} once (no reactive effects). Works in the browser (DOMParser) or in Node with optional peer linkedom.
import { renderToString } from "@sky.ui/reactivity";
const html = await renderToString(
"<!DOCTYPE html><html><body><p>{{ msg }}</p></body></html>",
{ msg: "Hello" },
{ stripAnchors: true } // default: remove <!--sky-for--> / <!--if-chain-->
);| Option | Default | Description |
|--------|---------|-------------|
| root | body | CSS selector for compile root |
| initial | — | Merged into context before compile |
| fullDocument | false | Return <html>…</html> instead of root outerHTML |
| stripAnchors | true | Remove compiler comment anchors from output |
There is no hydration yet — use SSR for static HTML, previews, or tests. For tooling/tests you can also use runHandlerCode(code, ctx, $event?, $el?).
CRUD: Create, Read, Update, Delete
All of these assume you use reactive objects/arrays (or refs holding them) so that the template and any effect/watch stay in sync.
Arrays
Create (add items):
// Add at end
ctx.items.push({ id: 1, name: "New" });
// Add at start
ctx.items.unshift({ id: 0, name: "First" });
// Add at index
ctx.items.splice(2, 0, { id: 2, name: "Middle" });Read:
- In template:
sky-for="item in items"and{{ item.name }}, or{{ items[0] }}, etc. - In JS:
ctx.items[i],ctx.items.length, or iterate as usual.
Update (in place):
// By index
ctx.items[0] = { id: 1, name: "Updated" };
// Find and update (same reference or replace)
const idx = ctx.items.findIndex((x) => x.id === id);
if (idx !== -1) ctx.items[idx] = { ...ctx.items[idx], name: "New Name" };Delete:
// By index (one item)
ctx.items.splice(index, 1);
// Remove by id (example)
const idx = ctx.items.findIndex((x) => x.id === id);
if (idx !== -1) ctx.items.splice(idx, 1);
// Clear all
ctx.items.length = 0;
// or
ctx.items.splice(0, ctx.items.length);Use sky-key (e.g. sky-key="item.id") when doing heavy add/remove/reorder so the engine can reuse DOM nodes correctly.
Objects
Create (add property):
ctx.user = { name: "Alice", age: 30 };
// or add to existing
ctx.user.role = "admin";Read:
- In template:
{{ user.name }},{{ user.age }}. - In JS:
ctx.user.name, etc.
Update:
ctx.user.name = "Bob";
ctx.user.age = 31;
// or replace whole object
ctx.user = { ...ctx.user, name: "Bob" };Delete:
delete ctx.user.role; // triggers reactivity (reactive() implements deleteProperty)Nested objects and arrays are made reactive when accessed, so mutations deep in the tree still trigger updates.
Nested structures
Keep the same reactive root; mutate nested data in place when possible so the engine tracks changes:
// Good: same reactive object, property update
ctx.state.filters.name = "x";
// Good: same reactive array, mutation
ctx.state.list.push(item);
// Also fine: replace a nested object entirely
ctx.state.filters = { name: "x", type: "a" };For arrays of objects, prefer updating one element by index or key rather than replacing the whole array if you want minimal DOM updates and stable keys in sky-for.
Expression context and ref unwrapping
Inside template expressions and event handler statements:
- Identifiers (e.g.
count,items) resolve to context properties. If the value is a ref, it is auto-unwrapped: you read and write the inner value (e.g.countmeanscount.value). - Member access (e.g.
user.name) also unwraps the receiver if it’s a ref, souser.nameworks whenuseris a ref. - Assignment to a ref in context (e.g.
count = 5) is compiled tocount.value = 5. - $event and $el are available in event handlers as the native event and the element.
- sky-for adds
item,$item,$index, and for objects$key, in that block’s scope.
So in templates you can write {{ count }}, count++, user.name, items.push(x) without using .value.
What is supported in expressions
- Literals, identifiers, property access (including computed like
obj[key]) - Optional chaining (
?.) and nullish coalescing (??) - Unary:
!,+,-,typeof, etc. - Binary:
+,-,*,/,%,**,==,===,!=,!==,<,<=,>,>=,&&,||,in,instanceof - Ternary:
a ? b : c - Assignment:
=,+=,-=, etc. - Increment/decrement:
++,-- - Function calls:
fn(),obj.method(), and spreadfn(...args) - Arrays and objects:
[a, b],{ key: value } - Template literals:
`Hello ${name}` - In statements (event handlers):
if/else,for/while/do…while,for…in/for…of,switch/break/continue,try/catch/finally,throw,return,var/let/const, blocks{ }, and top-levelawait(async path)
What is blocked or unsupported
- Blocked identifiers (cannot be used as variable names or in scope):
window,self,document,globalThis,Function,eval,constructor,__proto__,prototype,location,localStorage,sessionStorage,indexedDB,navigator,fetch,XMLHttpRequest,WebSocket. - Blocked properties:
__proto__,prototype,constructor. deletein expressions: No side effect (returnstrueonly).- SSR hydration, Teleport, slots, and full Vue SFC parity.
If you need to call fetch or use window, expose a function from setup and call it from the template, e.g. @click="loadData()".
Common patterns
Form with validation:
createReactivity(() => {
const form = reactive({ email: "", name: "" });
const error = ref("");
const submit = () => {
if (!form.email) {
error.value = "Email required";
return;
}
error.value = "";
// submit...
};
return { ...form, error, submit };
});<input sky-model="email" />
<span sky-show="error">{{ error }}</span>
<button onclick="submit()">Submit</button>List with add/remove and selection:
createReactivity(() => {
const items = reactive([
{ id: 1, name: "A" },
{ id: 2, name: "B" },
]);
const selectedId = ref(null);
const add = () => items.push({ id: Date.now(), name: "New" });
const remove = (id) => {
const i = items.findIndex((x) => x.id === id);
if (i !== -1) items.splice(i, 1);
};
return { items, selectedId, add, remove };
});<ul>
<li sky-for="item in items" sky-key="item.id">
{{ item.name }}
<button onclick="selectedId = item.id">Select</button>
<button onclick="remove(item.id)">Remove</button>
</li>
</ul>
<button onclick="add()">Add item</button>Filtering a list (computed):
const query = ref('');
const allItems = reactive([...]);
const filtered = computed(() =>
allItems.filter((x) => x.name.toLowerCase().includes(query.value.toLowerCase()))
);
return { query, filtered };<input sky-model="query" />
<ul>
<li sky-for="item in filtered" sky-key="item.id">{{ item.name }}</li>
</ul>Async load and show state:
const data = ref(null);
const loading = ref(false);
const load = async () => {
loading.value = true;
try {
const res = await fetch("/api/data");
data.value = await res.json();
} finally {
loading.value = false;
}
};
return { data, loading, load };<button onclick="load()">Load</button>
<div sky-if="loading">Loading...</div>
<div sky-else-if="data">{{ data }}</div>Development warnings
In development (__SKY_DEV__ is true by default except when NODE_ENV === 'production'), Sky prints Vue-style warnings:
[Sky warn]: sky-for without sky-key may recreate DOM nodes on list updates. Add sky-key="item.id" (or a stable key).
at <li#item sky-for="item in items">
(sky-for)warnOnce— same code + element only once (e.g. missingsky-key,sky-htmlXSS note).warn— every time (e.g. invalidsky-modelvalue).- Stable warning codes (e.g. missing
sky-key, invalidsky-model) — see package exports forWarnCodeswhen building custom tooling.
import { setSkyDev, setWarnHandler, WarnCodes } from "@sky.ui/reactivity";
setSkyDev(true); // force warnings on
setWarnHandler((code, message, ctx) => {
myLogger.warn(code, message, ctx);
});Troubleshooting
| Problem | What to check |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Template doesn’t update when I change data | Ensure the object/array is reactive (created with reactive() or returned from setup so the engine wrapped it). Replacing the whole reference (e.g. ctx.items = []) is fine; replacing a ref’s inner value without going through the ref can skip tracking. |
| Ref in template: “undefined” or wrong value | In templates you don’t use .value; the engine unwraps refs. If you see the ref object, you might be reading it in JS without .value, or the ref isn’t on the context. |
| sky-for not updating / wrong order | Use sky-key with a unique stable id. Prefer mutating the same array (push/splice) instead of replacing the whole array if you need stable DOM reuse. |
| “Blocked identifier” or “Blocked property” | Don’t use reserved globals or properties in expressions. Call a function from context that uses them in normal JS instead. |
| Event handler not firing | Use onclick/oninput/etc. or @click/@input; attribute names are case-sensitive. Ensure the attribute value is a valid statement. |
| sky-model not updating variable | The attribute value must be a single variable name (e.g. sky-model="username"). That name must exist on the context. |
| Need to run code after DOM update | Use await nextTick() after mutating state, then measure or call focus. |
| Memory leak / duplicate updates after navigation | Call unmount() when removing the app from the page so effects and listeners are cleared. |
Vue 3 comparison
Sky implements a subset of Vue 3’s reactivity and template ergonomics, not the full runtime (no components, VNodes, or hydration).
Implemented (close to Vue 3)
| API / feature | Notes |
|---------------|--------|
| ref, reactive, computed, watch, effect | Core tracking; refs unwrapped in templates |
| shallowRef, triggerRef, shallowReactive, shallowReadonly | Shallow variants |
| readonly, toRaw, markRaw, isReactive, isReadonly, isRef, unref | Proxies and guards |
| toRef, toRefs, customRef | Ref helpers |
| watchEffect, effectScope, getCurrentScope, onScopeDispose | Effects and grouping |
| nextTick, flushSync | Scheduling (flush runs after reactive jobs) |
| effect cleanup / pause / resume | On returned runner |
| Directives | sky-if chain, keyed sky-for (DOM move), sky-show, sky-model, sky-ref, sky-html, sky-once, :class / :style / sky-bind:* |
| Events | @event, modifiers, sync handlers + await in handlers |
| SSR | renderToString + optional linkedom |
Not implemented (Vue features)
- Component model,
provide/inject, Teleport, KeepAlive, transitions - SSR hydration and streaming
- Full template compiler (only in-DOM directives +
{{ }}) - Vue SFC / multi-model on one component beyond
sky-model:argsyntax
Summary
- Use createReactivity(setup).mount(target, initial) for apps; mount(root, ctx) for low-level control; renderToString for one-shot HTML.
- Prefer sky-key on sky-for; list updates move existing nodes when keys match.
- Handlers support if / loops / switch / try / await; use setup functions for heavy logic and browser APIs.
- sky-html is trusted-only; call unmount() when tearing down the app.
License
Sky UI Free EULA — proprietary; not open source.
