|
| 1 | +# Agera |
| 2 | + |
| 3 | +[![ESM-only package][package]][package-url] |
| 4 | +[![NPM version][npm]][npm-url] |
| 5 | +[![Dependencies status][deps]][deps-url] |
| 6 | +[![Install size][size]][size-url] |
| 7 | +[![Build status][build]][build-url] |
| 8 | +[![Coverage status][coverage]][coverage-url] |
| 9 | + |
| 10 | +[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg |
| 11 | +[package-url]: https://nodejs.org/api/esm.html |
| 12 | + |
| 13 | +[npm]: https://img.shields.io/npm/v/agera.svg |
| 14 | +[npm-url]: https://npmjs.com/package/agera |
| 15 | + |
| 16 | +[deps]: https://img.shields.io/librariesio/release/npm/agera |
| 17 | +[deps-url]: https://libraries.io/npm/agera/tree |
| 18 | + |
| 19 | +[size]: https://deno.bundlejs.com/badge?q=agera |
| 20 | +[size-url]: https://bundlejs.com/?q=agera |
| 21 | + |
| 22 | +[build]: https://img.shields.io/github/actions/workflow/status/TrigenSoftware/nanoviews/tests.yml?branch=main |
| 23 | +[build-url]: https://github.com/TrigenSoftware/nanoviews/actions |
| 24 | + |
| 25 | +[coverage]: https://img.shields.io/codecov/c/github/TrigenSoftware/nanoviews.svg |
| 26 | +[coverage-url]: https://app.codecov.io/gh/TrigenSoftware/nanoviews |
| 27 | + |
| 28 | +A small push-pull based signal library based on [alien-signals](https://github.com/stackblitz/alien-signals) algorithm. |
| 29 | + |
| 30 | +Was created as reactivity system for [nanoviews](https://github.com/TrigenSoftware/nanoviews/tree/main/packages/nanoviews) and [Kida](https://github.com/TrigenSoftware/nanoviews/tree/main/packages/kida). |
| 31 | + |
| 32 | +- **Small**. Around 1.37 kB for basic methods (minified and brotlied). Zero dependencies. |
| 33 | +- **Super fast** as [alien-signals](https://github.com/stackblitz/alien-signals). |
| 34 | +- Designed for best **Tree-Shaking**: only the code you use is included in your bundle. |
| 35 | +- **TypeScript**-first. |
| 36 | + |
| 37 | +```ts |
| 38 | +import { signal, computed, effect } from 'agera' |
| 39 | + |
| 40 | +const $count = signal(1) |
| 41 | +const $doubleCount = computed(() => count() * 2) |
| 42 | + |
| 43 | +effect(() => { |
| 44 | + console.log(`Count is: ${$count()}`); |
| 45 | +}) // Console: Count is: 1 |
| 46 | + |
| 47 | +console.log($doubleCount()) // 2 |
| 48 | + |
| 49 | +$count(2) // Console: Count is: 2 |
| 50 | + |
| 51 | +console.log($doubleCount()) // 4 |
| 52 | +``` |
| 53 | + |
| 54 | +<hr /> |
| 55 | +<a href="#install">Install</a> |
| 56 | +<span> • </span> |
| 57 | +<a href="#basics">API</a> |
| 58 | +<span> • </span> |
| 59 | +<a href="#differences-from-alien-signals">Differences from alien-signals</a> |
| 60 | +<br /> |
| 61 | +<hr /> |
| 62 | + |
| 63 | +## Install |
| 64 | + |
| 65 | +```bash |
| 66 | +pnpm add -D agera |
| 67 | +# or |
| 68 | +npm i -D agera |
| 69 | +# or |
| 70 | +yarn add -D agera |
| 71 | +``` |
| 72 | + |
| 73 | +## API |
| 74 | + |
| 75 | +### Signal |
| 76 | + |
| 77 | +Signal is a basic store type. It stores a single value. |
| 78 | + |
| 79 | +```ts |
| 80 | +import { signal, update } from 'agera' |
| 81 | + |
| 82 | +const $count = signal(0) |
| 83 | + |
| 84 | +$count($count() + 1) |
| 85 | +// or |
| 86 | +update($count, count => count + 1) |
| 87 | +``` |
| 88 | + |
| 89 | +To watch signal changes, use the `effect` function. Effect will be called immediately and every time the signal changes. |
| 90 | + |
| 91 | +```ts |
| 92 | +import { signal, effect } from 'agera' |
| 93 | + |
| 94 | +const $count = signal(0) |
| 95 | + |
| 96 | +const stop = effect(() => { |
| 97 | + console.log('Count:', $count()) |
| 98 | + |
| 99 | + return () => { |
| 100 | + // Cleanup function. Will be called before effect update and before effect stop. |
| 101 | + } |
| 102 | +}) |
| 103 | +// later you can stop effect |
| 104 | +stop() |
| 105 | +``` |
| 106 | + |
| 107 | +### Computed |
| 108 | + |
| 109 | +Computed is a signal that computes its value based on other signals. |
| 110 | + |
| 111 | +```ts |
| 112 | +import { computed } from 'agera' |
| 113 | + |
| 114 | +const $firstName = signal('John') |
| 115 | +const $lastName = signal('Doe') |
| 116 | +const $fullName = computed(() => `${$firstName()} ${$lastName()}`) |
| 117 | + |
| 118 | +console.log($fullName()) // John Doe |
| 119 | +``` |
| 120 | + |
| 121 | +### `effectScope` |
| 122 | + |
| 123 | +`effectScope` creates a scope for effects. It allows to stop all effects in the scope at once. |
| 124 | + |
| 125 | +```ts |
| 126 | +import { signal, effectScope, effect } from 'agera' |
| 127 | + |
| 128 | +const $a = signal(0) |
| 129 | +const $b = signal(0) |
| 130 | +const stop = effectScope(() => { |
| 131 | + effect(() => { |
| 132 | + console.log('A:', $a()) |
| 133 | + }) |
| 134 | + |
| 135 | + effectScope(() => { |
| 136 | + effect(() => { |
| 137 | + console.log('B:', $b()) |
| 138 | + }) |
| 139 | + }) |
| 140 | +}) |
| 141 | + |
| 142 | +stop() // stop all effects |
| 143 | +``` |
| 144 | + |
| 145 | +Also there is a possibility to create a lazy scope. |
| 146 | + |
| 147 | +```ts |
| 148 | +import { signal, effectScope, effect } from 'agera' |
| 149 | + |
| 150 | +const $a = signal(0) |
| 151 | +const $b = signal(0) |
| 152 | +// All scopes will run immediately, but effects run is delayed |
| 153 | +const start = effectScope(() => { |
| 154 | + effect(() => { |
| 155 | + console.log('A:', $a()) |
| 156 | + }) |
| 157 | + |
| 158 | + effectScope(() => { |
| 159 | + effect(() => { |
| 160 | + console.log('B:', $b()) |
| 161 | + }) |
| 162 | + }) |
| 163 | +}, true) // marks scope as lazy |
| 164 | +// start all effects |
| 165 | +const stop = start() |
| 166 | + |
| 167 | +stop() // stop all effects |
| 168 | +``` |
| 169 | + |
| 170 | +### Lifecycles |
| 171 | + |
| 172 | +One of main feature of Agera is that every signal can be active or inactive. It allows to create lazy signals, which will use resources only if signal is really used in the UI. |
| 173 | + |
| 174 | +- Signal is active when one or more effects is attached to it. |
| 175 | +- Signal is incative when signal has no effects. |
| 176 | + |
| 177 | +`onActivate` lifecycle method adds callback for activation and deactivation events. |
| 178 | + |
| 179 | +```ts |
| 180 | +import { signal, onActivate, effect } from 'agera' |
| 181 | + |
| 182 | +const $count = signal(0) |
| 183 | + |
| 184 | +onActivate($count, (active) => { |
| 185 | + console.log('Signal is', active ? 'active' : 'inactive') |
| 186 | +}) |
| 187 | + |
| 188 | +// will activate signal |
| 189 | +const stop = effect(() => { |
| 190 | + console.log('Count:', $count()) |
| 191 | +}) |
| 192 | +// will deactivate signal |
| 193 | +stop() |
| 194 | +``` |
| 195 | + |
| 196 | +### Batch updates |
| 197 | + |
| 198 | +To batch updates you should wrap signal updates between `startBatch` and `endBatch`. |
| 199 | + |
| 200 | +```ts |
| 201 | +import { signal, startBatch, endBatch, effect } from 'agera' |
| 202 | + |
| 203 | +const $a = signal(0) |
| 204 | +const $b = signal(0) |
| 205 | + |
| 206 | +effect(() => { |
| 207 | + console.log('Sum:', $a() + $b()) |
| 208 | +}) |
| 209 | + |
| 210 | +// Effects will be called only once |
| 211 | +startBatch() |
| 212 | +$a(1) |
| 213 | +$b(2) |
| 214 | +endBatch() |
| 215 | +``` |
| 216 | + |
| 217 | +### Skip tracking |
| 218 | + |
| 219 | +To skip tracking of signal changes you should wrap signal calls between `pauseTracking` and `resumeTracking`. |
| 220 | + |
| 221 | +```ts |
| 222 | +import { signal, pauseTracking, resumeTracking, effect } from 'agera' |
| 223 | + |
| 224 | +const $a = signal(0) |
| 225 | +const $b = signal(0) |
| 226 | + |
| 227 | +effect(() => { |
| 228 | + const a = $a() |
| 229 | + |
| 230 | + pauseTracking() |
| 231 | + |
| 232 | + const b = $b() |
| 233 | + |
| 234 | + resumeTracking() |
| 235 | + |
| 236 | + console.log('Sum:', a + b) |
| 237 | +}) |
| 238 | + |
| 239 | +// Will trigger effect run |
| 240 | +$a(1) |
| 241 | + |
| 242 | +// Will not trigger effect run |
| 243 | +$b(2) |
| 244 | +``` |
| 245 | + |
| 246 | +### Morph |
| 247 | + |
| 248 | +`morph` methods allows to create signals that can change their getter and setter on the fly. |
| 249 | + |
| 250 | +```ts |
| 251 | +import { signal, morph, $$get, $$set, $$source } from 'agera' |
| 252 | + |
| 253 | +const $string = signal('') |
| 254 | +// Debounce signal updates |
| 255 | +const $debouncedString = morph($string, { |
| 256 | + [$$set]: debounce($string, 300) |
| 257 | +}) |
| 258 | +// Lazy initialization |
| 259 | +const $lazyString = morph($string, { |
| 260 | + [$$get]() { |
| 261 | + this[$$set]('Lazy string') |
| 262 | + this[$$get] = this[$$source] |
| 263 | + return 'Lazy string' |
| 264 | + } |
| 265 | +}) |
| 266 | +``` |
| 267 | + |
| 268 | +### `isSignal` |
| 269 | + |
| 270 | +`isSignal` method checks if the value is a signal. |
| 271 | + |
| 272 | +```ts |
| 273 | +import { isSignal, signal } from 'agera' |
| 274 | + |
| 275 | +isSignal(signal(1)) // true |
| 276 | +``` |
| 277 | + |
| 278 | +## Differences from alien-signals |
| 279 | + |
| 280 | +Key differences from [alien-signals](https://github.com/stackblitz/alien-signals): |
| 281 | + |
| 282 | +- **Tree-shaking**. Agera is designed to be tree-shakable. Only the code you use is included in your bundle. Alien-signals is not well tree-shakable. |
| 283 | +- **Size**. Agera has a little bit smaller size for basic methods than alien-signals. |
| 284 | +- **Lifecycles**. Agera has lifecycles for signals. You can listen to signal activation and deactivation events. |
| 285 | +- **Modificated `effectScope`**. Agera has a possibility to put effect scope inside another effect scope. Also there is a possibility to create a lazy scope. |
0 commit comments