aided-core
v1.3.0
Published
A minimal JavaScript library for building user interfaces with fine-grained reactivity.
Maintainers
Readme
Aided
Aided is a minimal JavaScript library for building user interfaces with fine-grained reactivity. It updates the DOM directly without using a Virtual DOM, focusing on performance, simplicity, and an excellent developer experience.
Core Principles
- Fine-Grained Reactivity: A system of reactive primitives (
signals,effects,memos) ensures that when state changes, only the specific code that depends on it is re-executed. - Direct DOM Manipulation: Instead of computing and diffing virtual trees, Aided's effects are bound directly to DOM nodes, enabling precise, surgical updates.
- Render-Once Components: Components are plain JavaScript functions that execute once to create a DOM tree and set up reactive bindings. They do not re-render.
- Automatic Memory Management: An ownership graph, managed via
createRoot, tracks all nested reactive scopes and automatically cleans them up to prevent memory leaks. - Headless Logic: Complex UI logic (like virtualization) is separated into headless, framework-agnostic utilities, promoting reusability and clean component architecture.
Installation
yarn add aided-core
# or
npm install aided-coreGetting Started
Aided uses a hyperscript function, h, for a declarative and readable way to create DOM elements. It feels like JSX, but it's just plain JavaScript functions.
index.html
<div id="app"></div>
<script type="module" src="./main.js"></script>main.js
import { render, createSignal, h } from "aided-core";
function Counter() {
const [count, setCount] = createSignal(0);
// Use the `h` helper to build your UI
return h.button(
{
// Event handlers are passed as props
onClick: () => setCount(count() + 1),
},
// Reactive children are automatically updated
"Count: ",
count,
);
}
// Mount the component to the DOM
render(Counter, document.getElementById("app"));Interactive Playground
Explore Aided's capabilities with our comprehensive interactive playground located in the playground/ directory. The playground includes:
- Live Examples: Interactive demos of all major features including signals, effects, memos, and components
- Real-World Patterns: Complete implementations of common UI patterns like forms, modals, notifications, and virtualized lists
- Component Showcase: Working examples of all structural components (
For,Show,VirtualFor, etc.) - Advanced Patterns: Demonstrations of complex reactivity patterns including context, portals, and state isolation
- In‑Browser Documentation: Full API reference, guides, and source‑code walkthroughs rendered from Markdown with syntax highlighting, copy buttons, and a navigable sidebar
To run the playground:
cd playground
yarn install
yarn devThe playground serves as both a learning resource and a testing ground for new features, showcasing best practices and real-world usage patterns.
API Reference
Core Primitives
createSignal<T>(initialValue: T, options?: ReactiveOptions): [SignalGetter<T>, SignalSetter<T>]
Creates a reactive state container. Returns a tuple containing a getter and a setter.
createEffect(fn: () => void, options?: ReactiveOptions): Disposer
Creates a reactive scope that automatically re-runs when its dependencies change.
createMemo<T>(fn: () => T, options?: ReactiveOptions): Memo<T>
Creates a derived, read-only signal that caches its value.
untrack<T>(fn: () => T): T
New in v1.1.0 - Executes a function without tracking its dependencies, preventing the current reactive scope from re-running when signals inside the function change.
Useful for:
- Creating component instances that maintain independent state
- Performing side effects without triggering reactive updates
- Breaking unwanted dependency chains
const [count, setCount] = createSignal(0);
// This effect normally re-runs when count changes
createEffect(() => {
console.log("Count:", count());
});
// This won't trigger the effect
untrack(() => {
console.log("Silent read:", count()); // Not tracked
});createResource<S, T>(source: SignalGetter<S>, fetcher: Fetcher<S, T>): Resource<T>
Creates a signal for managing asynchronous data, complete with reactive .loading and .error states.
New in v1.3.0: The fetcher now receives a second argument info: { signal: AbortSignal } – an AbortSignal that aborts when the resource is disposed or the source changes. This allows you to cancel in‑flight requests cleanly.
const [userId] = createSignal(1);
const user = createResource(userId, async (id, { signal }) => {
const res = await fetch(`/api/users/${id}`, { signal });
return res.json();
});longestIncreasingSubsequenceAsync(array: Int32Array): Promise<number[]>
High-performance async implementation of the longest increasing subsequence algorithm for large arrays, using Web Workers for non-blocking computation.
configureLIS(options: { smallArrayThreshold: number }): void
Configures the threshold for switching between fast and optimized LIS algorithms.
AidedError
Custom error class for better debugging of reactive issues.
Lifecycle & Context
createRoot(fn: (dispose: Disposer) => void): Disposer
Creates a top-level ownership scope for automatic memory management.
onCleanup(fn: Disposer): void
Registers a cleanup function to run when the current reactive scope is disposed. Essential for cleaning up event listeners, timers, and other resources.
createEffect(() => {
const timer = setInterval(() => console.log("tick"), 1000);
onCleanup(() => {
clearInterval(timer); // Clean up when effect re-runs or scope ends
});
});createContext<T>(defaultValue?: T): Context<T>
Creates a context object for providing data throughout a component tree.
provide<T>(context: Context<T>, value: T): void
Provides a value for a context within the current scope.
useContext<T>(context: Context<T>): T | undefined
Consumes a value from the nearest context provider.
Rendering & DOM
render(component: () => Element, mountNode: Element): Disposer
The main entry point for an application. It mounts a component into a DOM node within a new reactive root.
h (Hyperscript)
The h helper is the primary way to build UI in Aided. It's a proxy that provides a function for every HTML tag (e.g., h.div, h.a).
import { h, createSignal } from "aided-core";
const [name, setName] = createSignal("World");
const [isActive, setIsActive] = createSignal(true);
const element = h.div(
// Attributes and event handlers go in an object
{
id: "container",
classList: { active: isActive, static: true },
style: { color: () => (isActive() ? "blue" : "grey") },
onClick: () => console.log("Clicked!"),
},
// Children follow the attributes
"Hello, ",
name,
);- Reactive Children: Passing a signal (
name) as a child automatically creates a reactive text node. - Reactive Attributes: Passing a signal as an attribute value (
id: myId) creates a reactive binding. - Special Properties:
hhas special handling forclassList,style,ref, and event handlers (onClick,onInput, etc.).
Model (Two-Way Binding)
The Model helper provides two-way binding for form inputs. It's used with the ref property in the h helper.
const nameSignal = createSignal("");
const input = h.input({
ref: (el) => Model(el, nameSignal),
});Structural Components
Show(props: { when, children, fallback? }): Node
Conditionally renders children if when() is truthy, otherwise renders fallback.
Show({
when: isLoggedIn,
fallback: () => h.p("Please log in."),
children: () => h.p("Welcome!"),
});For(props: { each, key?, children }): Node
Efficiently renders a list of items using a keyed reconciliation algorithm.
const [items] = createSignal(["a", "b"]);
const list = h.ul(
For({
each: items,
key: (item) => item,
children: (item) => h.li(item),
}),
);createVirtualizer<T>(options): Virtualizer<T>
Creates a headless, high-performance engine for virtualizing large lists. It contains all the state and logic for calculating the visible window of items, which can then be used by a rendering component.
options.items: A signal containing the full list of data.options.itemHeight: The fixed height of each item in pixels.options.overscan: The number of extra items to render on either side of the visible area.
Returns a Virtualizer object with reactive properties:
.visibleItems: A memoized array of the items that should be rendered..totalHeight: A memoized total height of the scrollable area..visibleState: A memoized object containing{ startIndex, endIndex, scrollOffset }..setContainer: A function to pass the scrollable container element to the virtualizer.
VirtualFor<T>(props): HTMLElement
An efficient, high-performance component for rendering virtualized lists. It renders only the items currently visible in the scrollable area, making it suitable for lists with thousands or millions of rows.
const [items] = createSignal(
Array.from({ length: 100000 }, (_, i) => `Item ${i}`),
);
// Simple usage
const list = VirtualFor({
each: items,
itemHeight: 30, // Each row is 30px high
overscan: 5, // Render 5 extra items above/below the viewport
children: (item, index) => h.div({ class: "row" }, `${index}: ${item}`),
});
// With container customization
const customList = VirtualFor({
each: items,
itemHeight: 30,
overscan: 5,
containerProps: {
className: "my-scroller",
style: { height: "400px", border: "1px solid #ccc" },
attributes: [
{ name: "data-testid", value: "virtual-list" },
{ name: "aria-label", value: "Virtualized item list" },
],
},
children: (item, index) => h.div({ class: "row" }, `${index}: ${item}`),
});Props:
each: A signal containing the array of items.itemHeight: The fixed height of each item in pixels.children: A function that receives the item and itsindexand returns a DOM node.overscan?: The number of extra items to render on either side (default: 5).containerProps?: Optional configuration for the scroll container:className?: CSS class name(s) for the containerstyle?: Inline styles for the containerattributes?: Array of custom attributes (e.g.,[{ name: 'data-testid', value: 'list' }])
placeholder?: An optional element to show when theeacharray is empty.
Note: The attributes array accepts Attribute objects with name and value properties. Dangerous attributes like ref, role, style, class, and className are automatically filtered for safety.
Fragment(props: { children: Node[] }): DocumentFragment
Groups multiple children without adding a wrapper element to the DOM.
Portal(props: { mount: Element, children: Node }): Comment
Renders children into a different DOM mount node.
Performance & Trade-offs
Aided achieves exceptional performance through:
- Direct DOM Manipulation: No Virtual DOM diffing - effects bind directly to DOM nodes
- Fine-Grained Updates: Only code dependent on changed state re-executes
- Surgical Reconciliation: The
Forcomponent uses an optimized LIS algorithm for minimal DOM operations - Virtual Scrolling:
VirtualForrenders only visible items for millions of rows
Bundle Size: 5.92kb minified + gzipped Memory: Automatic cleanup prevents leaks through ownership graph Runtime: Zero dependencies, pure JavaScript execution
The trade-off is that it does not use JSX, instead opting for a hyperscript function (h) for UI creation. The keyed reconciliation in For is highly efficient for lists with stable keys. For extremely large datasets, the VirtualFor component provides best-in-class rendering performance.
Security Strengthening (v1.2.0)
Version 1.2.0 introduced proactive security measures to prevent common injection attacks.
bindAttr – Event Handler Rejection
The bindAttr utility now rejects any attribute name starting with on (case‑insensitive), such as onclick, onload, onerror. Trying to bind such attributes throws a descriptive error:
// ❌ This now throws an error
bindAttr(button, "onclick", () => console.log("clicked"));
// Error: Security: Cannot bind event handler attribute 'onclick'. Use addEventListener() instead.Always use addEventListener or the onClick prop in h() for event handling.
h Proxy – Tag Name Validation
The h hyperscript helper now validates all tag names and blocks dangerous ones:
- Blocked tags:
script,constructor,prototype(throws aSecurityerror). - Tag name format: Must start with a letter and contain only letters, numbers, and hyphens:
/^[a-zA-Z][a-zA-Z0-9-]*$/.
// ❌ These now throw errors
h.script(); // Security: Cannot create 'script' element...
h["1div"](); // Invalid tag name '1div'...
h["constructor"](); // Security: Cannot create 'constructor' element...
// ✅ Still allowed
h.div();
h["my-custom-element"]();For Component – Null Children Support
The children function of the For component can now return null (or undefined) to skip rendering an item entirely. The component properly disposes the reactive root for that item, preventing memory leaks.
For({
each: items,
children: (item) => {
if (item().hidden) return null; // skip this item
return h.div({}, item().text);
},
});This is especially useful for conditional rendering inside lists.
What’s New in v1.3.0
Performance Profiler (Zero‑Overhead)
A new built‑in profiler lets you track effect execution times without affecting production performance. When disabled, it adds only a single boolean check.
import { enableProfiler, getProfilerReport } from "aided-core";
enableProfiler(true);
// ... run your app ...
const report = getProfilerReport();
console.log(report.effectExecutions); // total number of effect runs
console.log(report.effects); // per‑effect counts and total timecreateResource with AbortSignal
The fetcher now receives an AbortSignal as part of the second argument, allowing you to cancel in‑flight requests when the resource is re‑fetched or the owning scope is disposed.
const user = createResource(
() => userId(),
async (id, { signal }) => {
const res = await fetch(`/api/users/${id}`, { signal });
return res.json();
},
);Stronger XSS Prevention – URL Protocol Blocking
bindAttr now blocks dangerous URL protocols (javascript:, vbscript:, data:) on URL‑sensitive attributes (href, src, action, formaction, xlink:href, srcdoc, poster). Both static and reactive values are validated, and a clear Security error is thrown.
h.dangerous – Explicit Escape Hatch
The h proxy now provides a .dangerous namespace for creating tags that are blocked by default (e.g., script, iframe, base, meta, link, object, embed). This forces developers to explicitly opt in and acknowledges the security risk; a warning is logged in development.
// ❌ Blocked by default
h.script(); // throws
// ✅ Explicit opt‑in
h.dangerous.script(); // works (with dev warning)The namespace still hard‑blocks prototype escapes (constructor, prototype, __proto__).
Scheduler Error Isolation
Errors thrown inside effects are now caught and logged individually. One failing effect no longer prevents other dirty effects from executing during the same flush batch. The log includes the effect name for easier debugging.
Portal DocumentFragment Fix
Portal now correctly handles DocumentFragment children, tracking individual child nodes and preventing NotFoundError crashes during cleanup.
Testing
Aided includes comprehensive test coverage to ensure reliability and correctness:
Unit Tests
The core library has extensive unit tests covering all reactive primitives, components, and utilities. Tests are written using Vitest and achieve high code coverage.
yarn testE2E Tests
End-to-end tests validate the complete user experience in real browsers using TestCafe. These tests run against the interactive playground and verify:
- Navigation and routing
- Interactive examples functionality
- Reactive state updates
- Cross-browser compatibility (Chrome, Firefox)
# Run all E2E tests
yarn test:e2e
# Run in specific browser
yarn test:e2e:chrome
yarn test:e2e:firefox
# Run in headed mode (see browser)
yarn test:e2e:headedE2E tests use the Page Object Model pattern for maintainability and include comprehensive coverage of all playground examples. See e2e/README.md for detailed documentation.
Contributing
Contributions are welcome! Please open an issue to discuss your ideas before submitting a pull request. See the CONTRIBUTING.md for more details on how to get started.
Acknowledgements
The architecture and API of Aided are heavily inspired by the excellent work of SolidJS and its fine-grained reactivity model.
