Signals: a 1 kB reactivity primitive for UI libraries to build on
A tiny (~1kB minzipped) and extremely fast library for creating reactive observables via functions.
At a glance
- What is it?
- @maverick-js/signals is an MIT licensed TypeScript library of reactive observables built from functions, with signals, computed values and effects, batched updates on a microtask scheduler and a lazy evaluation model borrowed from Svelte. It is explicitly aimed at other UI libraries rather than at application developers, and it is the reactivity layer you build a renderer on top of.
- Who is it for?
- Signals fits a team writing a UI library or framework that needs a reactivity core it controls, since the whole design is a small primitive with an explicit lazy model, a scope system for disposal, and a batched scheduler, and the project is explicit that it is a foundation for other libraries rather than an end-user state manager.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 45 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A library for library authors, stated up front
The README opens with the intent, and it is worth reading before anything else. The goal of this library is to provide a lightweight reactivity API for other UI libraries to be built on top of. It follows the lazy principle that Svelte adheres to, don't do any unnecessary work and don't place the burden of figuring it out on the developer.
That positioning is the single most useful fact about the project. This is not a state manager for your application, and it is not a framework. It is the reactive primitive layer that a renderer sits on, and the API surface is designed to be small enough to embed in something else. That also means the people who benefit most are library authors, and the people who will find it least useful are application developers with a state problem and no intention of building a renderer.
The feature list in the README is short enough to take in at a glance. It is light, at roughly 1 kB minzipped. It works in both browsers and Node.js. All types are observable, so a string, an array or an object can be a signal. It only updates when the value has changed. Updates are batched via a microtask scheduler. It is lazy by default and efficiently re-computes only what is needed. Computations go through `computed`, effect subscriptions through `effect`, there are debugging identifiers, and it is strongly typed because it is built with TypeScript.
The topics attached to the repository list the project as fast, functional, isomorphic, lightweight, observables, reactive, reactivity and tiny, which is an accurate summary of the design intent.
The primary language is TypeScript, the package is at version 6.0.0, and the licence is MIT with a LICENSE file at the root of the tree. The repository has no GitHub releases, so the CHANGELOG.md is the version history.
The signal, computed and effect trio, and why tick exists
The core API is three functions, and the demo in the README shows how they relate.
A signal wraps a value. Calling it reads the current value, and it has a set method for writing, which accepts either a new value or a function of the previous one. A computed creates a new signal whose value is derived from a function, and that function is only re-run when one of its dependencies is updated, with dependencies being all the signals read during execution. An effect invokes a function each time any of the signals read inside it change, and it runs immediately on initialization.
import { signal, computed, tick } from '@maverick-js/signals';
const $a = signal(10);
const $b = signal(10);
const $c = computed(() => $a() + $b());
console.log($c()); // logs 20
$a.set(20);
tick();
console.log($c()); // logs 30
$b.set(20);
tick();
console.log($c()); // logs 40
// Nothing changed - no re-compute.
console.log($c()); // logs 40The tick call is the part that surprises newcomers, and the README warns about it directly under the signal section. Updates are batched on the microtask queue, so a write does not immediately run the effects that depend on it. The demo calls tick to flush the queue synchronously so the effect runs, and the same applies to computed reads, where the recomputation is likewise deferred until the batch is flushed.
That batching is a design decision rather than an accident, and it is the right one for a UI library. A renderer that sets ten properties before a frame should not trigger ten effect runs; it should trigger one. The cost is that the semantics are less obvious than a synchronous store, and that the library is choosing the browser's microtask queue as the place where the batching happens.
The lazy part is the other half. A computed is not eagerly kept up to date; it is recomputed on read, and only when something it depends on has actually changed. The demo's final comment says it plainly: nothing has changed, no re-compute. For a library author, that means a computed that is never read after a change costs nothing at all, which is a property that most store-based reactivity does not have.
root, orphans, and the scope rules that keep memory bounded
The scope system is the part of the API that a library author has to get right, and the README spends a good deal of space on it.
Computations are generally child computations, and when their respective parent scope is destroyed so are they. You can create orphan computations, which have no parent, and the README is clear that orphans will live in memory until their internal object references are garbage collected. The example shows an orphan computed whose only reference is inside an object, and the comment notes that it is GC'd when the object is.
The consequence the README draws is that orphans can make it hard to determine when a computation is disposed, so you will generally want to ensure you only create child computations. The root function is the tool for that. It stores all inner computations as children and hands you a function that disposes of them all at once.
import { root, signal, computed, effect } from '@maverick-js/signals';
root((dispose) => {
const $a = signal(10);
const $b = computed(() => $a());
effect(() => console.log($b()));
// Disposes of `$a`, $b`, and `effect`.
dispose();
});The root function also returns the result of the function it is given, so it can be used for its value as well as for its lifetime management.
Two supporting functions exist for the same reason. `peek` reads the current value inside a compute function whilst disabling observer tracking, so the read does not create a dependency, and `untrack` disables both scope and observer tracking. The difference matters: peek prevents a dependency from being tracked, untrack additionally prevents a scope from being tracked, so a signal created inside an untracked block becomes an orphan.
The API list in the README is longer than the three core functions, and these scoping functions are the ones that decide whether a long-lived application leaks. getScope, scoped, getContext and setContext are also there, along with onError, onDispose, readonly, computedMap, computedKeyedMap, isReadSignal and isWriteSignal.
What the benchmarks compare against, and how the package is built
The package.json shows how the project measures itself and who it measures against.
The devDependencies include @preact/signals-core, @reactively/core, s-js and solid-js, along with cellx and cli-table and kleur for output formatting. Those four are the comparison set: Preact signals, the Reactively core, s-js and Solid. The presence of cellx and cli-table suggests the benchmarks print comparison tables, and the bench/ directory at the repository root holds the code, with a bench:layers script that runs bench/layers.js.
The README's own benchmark section is not in the visible text, so the numbers are not quoted here. What can be said from the repository is the shape of the comparison: this library is arguing that it can match libraries from three different reactivity lineages, Preact, Solid and s-js, on size and speed, while adding a scope model the others do not have in the same form.
The build is Rollup with an esbuild plugin, producing a dist/ directory, and types are generated separately by tsc against tsconfig.build.json. The package is type module, marks sideEffects as false, publishes types at dist/types/index.d.ts and the ES module at dist/prod/index.js, and the exports map includes a test condition pointing at dist/dev/index.js, so the development build can be swapped in for debugging.
The test setup is more interesting than it looks. There is a test script running vitest, and separately a test:gc script that runs node with --expose-gc against vitest.js, with a watch variant. A library whose central claim is that orphans are collected by the garbage collector needs a test that forces collection and asserts on it, and that is exactly what the --expose-gc flag enables. The validate script chains the whole thing, running the tests, then the GC tests, then the build, and the release script runs validate before standard-version and the publish.
Version 6.0.0 with no GitHub releases, and what the tree says about the project
On maintenance, the repository is not archived and the last push was on 2026-09-09, on the main branch. The recent releases list is empty because the repository has no GitHub releases.
The version in package.json is 6.0.0, which tells you something the absence of tags does not. A major version at 6 means the API has been through several breaking changes, and the major number is doing the versioning work that GitHub tags would otherwise do. The CHANGELOG.md at the root of the tree is the record, and since the release script uses standard-version, the changelog and the version bump are generated together as part of the release flow, which is why the tags are absent from GitHub without losing the history.
The repository layout is small and tells you the project is a library rather than an application. There is src/, tests/, bench/, a rollup.config.js, a vite.config.ts, a vitest.js, a tsconfig.json and a tsconfig.build.json, a .prettierrc.cjs, a CHANGELOG.md, a LICENSE and a map.d.ts.
The contributors field names Rahim Alwer, and the project sits under the maverick-js organisation, which is the same scope as the earlier SwiftUI-inspired reactivity work in that ecosystem. The library is MIT licensed, so using it inside a commercial UI library carries no copyleft obligation, which is the point of a foundation layer.
One gap worth naming: the README's visible portion does not explain how to write a renderer on top of the primitives. The API list is complete and the semantics of each function are documented, but the work of connecting a signal to a DOM node, or to a native view, is left to the adopter. For a library author that is arguably the correct division, and it does mean you should budget time for the integration rather than expecting a ready-made binding.
Editorial conclusion
Signals fits a team writing a UI library or framework that needs a reactivity core it controls, since the whole design is a small primitive with an explicit lazy model, a scope system for disposal, and a batched scheduler, and the project is explicit that it is a foundation for other libraries rather than an end-user state manager. It is the wrong choice for an application that just needs shared state, because Preact signals or a framework's own reactivity will cover that with less API surface, and it is the wrong choice if you want a large, well-documented ecosystem around your state layer. Before you build on it, read the API list in the README, in particular root, peek and untrack, and check the version number, because the package is at 6.0.0 while the repository publishes no GitHub releases, so the changelog is your only upgrade record.
Frequently asked questions
How do I install @maverick-js/signals?
The README gives three commands: `npm i @maverick-js/signals`, `pnpm i @maverick-js/signals` and `yarn add @maverick-js/signals`. The package is ESM, marked type module, and works in both browsers and Node.js.
What is the difference between tick and a synchronous update?
Updates are batched on the microtask queue, so a write does not immediately run dependent effects or recompute a computed value. The README's examples call `tick()` to flush the queue synchronously, and the signal section carries a warning to read the tick section to understand batched updates.
What is the difference between peek and untrack?
peek returns the current value inside a compute function whilst disabling observer tracking, so the read does not create a dependency. untrack disables both scope and observer tracking, so a signal created inside an untracked block also becomes an orphan.
How do I dispose of signals and effects in @maverick-js/signals?
Computation scopes are created with `root`, which stores inner computations as children and returns a dispose function that tears them all down at once. The README notes that orphan computations with no parent live in memory until garbage collected, so child computations are generally preferable.
Is the signals library still being updated?
The repository is not archived and the last push was on 2026-09-09, with the package at version 6.0.0 and the licence MIT. The repository has no GitHub releases, so the CHANGELOG.md is the release history.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/maverick-js-signals)Was this useful?