Skip to content

Latest commit

 

History

History

README.md

Agera

ESM-only package NPM version Dependencies status Install size Build status Coverage status

Halftone sun logo

A small push-pull based signal library based on alien-signals algorithm.

Was created as reactivity system for nanoviews and Kida.

  • Small. Around 1.8 kB for basic methods (minified and brotlied). Zero dependencies.
  • Super fast as alien-signals.
  • Designed for best Tree-Shaking: only the code you use is included in your bundle.
  • TypeScript-first.
import { signal, computed, effect } from 'agera'

const $count = signal(1)
const $doubleCount = computed(() => count() * 2)

effect(() => {
  console.log(`Count is: ${$count()}`);
}) // Console: Count is: 1

console.log($doubleCount()) // 2

$count(2) // Console: Count is: 2

console.log($doubleCount()) // 4

Install   •   API   •   Why?

Install

pnpm add agera
# or
npm i agera
# or
yarn add agera

API

Signal

Signal is a basic store type. It stores a single value.

import { signal } from 'agera'

const $count = signal(0)

$count($count() + 1)
// or
$count(count => count + 1)

To watch signal changes, use the effect function. Effect will be called immediately and every time the signal changes.

import { signal, effect } from 'agera'

const $count = signal(0)

const stop = effect(() => {
  console.log('Count:', $count())

  return () => {
    // Cleanup function. Will be called before effect update and before effect stop.
  }
})
// later you can stop effect
stop()

Computed

Computed is a signal that computes its value based on other signals.

import { computed } from 'agera'

const $firstName = signal('John')
const $lastName = signal('Doe')
const $fullName = computed(() => `${$firstName()} ${$lastName()}`)

console.log($fullName()) // John Doe

effectScope

effectScope creates a scope for effects. It allows to stop all effects in the scope at once.

import { signal, effectScope, effect } from 'agera'

const $a = signal(0)
const $b = signal(0)
const stop = effectScope(() => {
  effect(() => {
    console.log('A:', $a())
  })

  effectScope(() => {
    effect(() => {
      console.log('B:', $b())
    })
  })
})

stop() // stop all effects

deferScope

Also there is a possibility to create a defer scope.

import { signal, deferScope, effectScope, effect } from 'agera'

const $a = signal(0)
const $b = signal(0)
// All scopes will run immediately, but effects run is delayed
const start = deferScope(() => {
  effect(() => {
    console.log('A:', $a())
  })

  effectScope(() => {
    effect(() => {
      console.log('B:', $b())
    })
  })
}, true) // marks scope as lazy
// start all effects
const stop = start()

stop() // stop all effects

subscribe

subscribe subscribes to accessor changes. Callback will be called immediately with the current value and on every subsequent change. Will trigger accessor mount if applicable.

import { signal, subscribe } from 'agera'

const $count = signal(0)

const stop = subscribe($count, (value) => {
  console.log('Count:', value)
})
// Console: Count: 0

$count(1)
// Console: Count: 1

stop()

listen

listen listens to accessor changes. Callback will be called only on value change, without initial call. Will trigger accessor mount if applicable.

import { signal, listen } from 'agera'

const $count = signal(0)

const stop = listen($count, (value) => {
  console.log('Count changed:', value)
})
// No console output

$count(1)
// Console: Count changed: 1

stop()

observe

observe observes accessor changes. Callback will be called only on value change, without initial call. Unlike subscribe and listen, it will not trigger accessor mount.

import { signal, mountable, observe, onMounted } from 'agera'

const $count = mountable(signal(0))

onMounted($count, (mounted) => {
  console.log('Signal is', mounted ? 'mounted' : 'unmounted')
})

const stop = observe($count, (value) => {
  console.log('Count changed:', value)
})
// No mount event, no console output

$count(1)
// Console: Count changed: 1

stop()

Lifecycles

One of main feature of Agera is that you can create mountable signals. It allows to create lazy signals, which will use resources only if signal is really used in the UI.

  • Signal is mounted when one or more effects is attached to it.
  • Signal is unmounted when signal has no effects.

mountable method makes signal mountable.

onMounted lifecycle method adds callback for mount and unmount events.

import { signal, mountable, onMounted, effect } from 'agera'

const $count = mountable(signal(0))

onMounted($count, (mounted) => {
  console.log('Signal is', mounted ? 'mounted' : 'unmounted')
})

// will mount signal
const stop = effect(() => {
  console.log('Count:', $count())
})
// will unmount signal
stop()

Batch updates

To batch updates you should wrap signal updates in batch function.

import { signal, batch, effect } from 'agera'

const $a = signal(0)
const $b = signal(0)

effect(() => {
  console.log('Sum:', $a() + $b())
})

// Effects will be called only once
batch(() => {
  $a(1)
  $b(2)
})

Skip tracking

To skip tracking of signal changes you should wrap signal into untracked function.

import { signal, untracked, effect } from 'agera'

const $a = signal(0)
const $b = signal(0)

effect(() => {
  const a = $a()

  untracked(() => {
    const b = $b()

    resumeTracking()

    console.log('Sum:', a + b)
  })
})
// or short variant
effect(() => {
  console.log('Sum:', $a() + untracked($b))
})

// Will trigger effect run
$a(1)

// Will not trigger effect run
$b(2)

Morph

morph methods allows to create signals that can change their getter and setter on the fly.

import { signal, morph } from 'agera'

const $string = signal('')
// Debounce signal updates
const $debouncedString = morph($string, {
  set: debounce($string, 300)
})
// Lazy initialization
const $lazyString = morph($string, {
  get() {
    this.set('Lazy string')
    this.get = this.source
    return 'Lazy string'
  }
})

isSignal

isSignal method checks if the value is a signal.

import { isSignal, signal } from 'agera'

isSignal(signal(1)) // true

Why?

Key differences from alien-signals:

  • 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.
  • Size. Agera has a little bit smaller size for basic methods than alien-signals.
  • Lifecycles. Agera has lifecycles for signals. You can listen to signal activation and deactivation events.
  • Modificated effectScope. Agera has a possibility to put effect scope inside another effect scope. Also there is a possibility to create a lazy scope.
  • Effects supports cleanup function.