npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 linkedom

Optional: 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


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: Prefer createReactivity(() => { 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 under target (selector string or Element), creates context from setup (and initial merged 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 to ctx.
  • Returns: The same ctx you 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 updates

ref

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 }): void

Example:

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 trigger

triggerRef

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 0

effect

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 }): () => T
  • lazy: If true, 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 1

watch

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' }
): () => void
  • source: 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, newVal and oldVal are arrays. onCleanup(fn) registers a cleanup that runs before the next run (e.g. cancel a prior request).
  • opts.immediate: If true, run the callback once immediately (with oldVal undefined).
  • opts.deep: If true, 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 when expr is truthy. Same idea as v-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"item is the loop variable, items is the expression (array, object, string, or number).
  • sky-for="(item, index) in items" — Same, plus index as 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 oninput yourself 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

  • :class or sky-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).

  • :style or sky-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 assigns node[propName] on every change (reactive effects).
  • The segment after sky-bind: may be camelCase or kebab-case (e.g. sky-bind:visible-itemsvisibleItems on 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. count means count.value).
  • Member access (e.g. user.name) also unwraps the receiver if it’s a ref, so user.name works when user is a ref.
  • Assignment to a ref in context (e.g. count = 5) is compiled to count.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 spread fn(...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-level await (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.
  • delete in expressions: No side effect (returns true only).
  • 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. missing sky-key, sky-html XSS note).
  • warn — every time (e.g. invalid sky-model value).
  • Stable warning codes (e.g. missing sky-key, invalid sky-model) — see package exports for WarnCodes when 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:arg syntax

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.