Giter VIP home page Giter VIP logo

ulive's Introduction

ULive test

Version Badge size Badge size

yarn: yarn add ulive

npm: npm i ulive

cdn: https://esm.sh/ulive

module: https://esm.sh/ulive?module

  • Small. 280 bytes gzip.
  • Fast.
  • Simple API
  • Reactive. Automatic derivation.
  • Value Ref Syntax
  • Circular Detection

API

signal(val)

Create a reactive or live state.

import { signal, computed, memo, effect } from "ulive";

const num = signal(0);
num.value = 10;
console.log(num.value);

effect(fn)

Run fn with automatic dependency check & cleanup return.

let num = signal(0);
effect(() => console.log(num.value));

computed(fn)

Returns computed value.

let num = signal(0);
let square = computed(() => num.value * num.value);
let cube = computed(() => square.value * num.value);
effect(() => console.log(num.value, square.value, cube.value));

batch(fn)

Defer effects.

let a = signal(0), b = signal(1)
let mul = computed(() => a.value * b.value);
effect(() => console.log(a.value, b.value, mul.value));
batch(() => (a++, b++));

untracked(fn)

Run without effects.

let a = signal(0), b = signal(1)
let mul = computed(() => a.value * b.value);
effect(() => untracked(() => console.log(a.value)));

toJSON or then or valueOf

const counter = signal(0);
const effectCount = signal(0);

effect(() => {
	console.log(counter.value);
	// Whenever this effect is triggered, increase `effectCount`.
	// But we don't want this signal to react to `effectCount`
	effectCount.value = effectCount.valueOf() + 1;
});

Usage

const num = signal(1);
let square = computed(() => num.value * num.value);
let cube = computed(() => square.value * num.value);
effect(() => console.log(num.value, square.value, cube.value));
num.value = 1;
num.value = 2;
num.value = 3;

Thanks and Inspiration

License

MIT

ulive's People

Contributors

dy avatar kethan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

ulive's Issues

Take over sprae/signal

Hi @kethan !

That might probably sound too bold, but would you like to just take sprae/signal implementation?
It was anyways based on ulive, just some bits of rewrite.
I believe o and memo functions don't belong to signals, mixing patterns doesn't do good here, and as you see from #2 they're not very elegant fit.

// Minimorum signals impl (almost _ulive_ without o/memo)
export let current,
  signal = (v, s, obs = new Set) => (
    s = {
      get value() {
        current?.deps.push(obs.add(current));
        return v
      },
      set value(val) {
        v = val
        for (let sub of obs) sub(val) // notify effects
      },
      peek() { return v },
    },
    s.toJSON = s.then = s.toString = s.valueOf = () => s.value,
    s
  ),
  effect = (fn, teardown, run, deps) => (
    run = (prev) => {
      teardown?.call?.()
      prev = current, current = run
      try { teardown = fn() } finally { current = prev }
    },
    deps = run.deps = [],

    run(),
    (dep) => { teardown?.call?.(); while (dep = deps.pop()) dep.delete(run) }
  ),
  computed = (fn, s = signal(), c, e) => (
    c = {
      get value() {
        e ||= effect(() => s.value = fn())
        return s.value
      }
    },
    c.toJSON = c.then = c.toString = c.valueOf = () => c.value,
    c
  ),
  batch = (fn) => fn(),
  untracked = (fn, prev, v) => (prev = current, current = null, v = fn(), current = prev, v)

Why I am asking - I want to make signals optional for sprae, and ulive would be the first library in the list, providing absolutely minimal implementation (306 bytes gzipped):

let e;export let signal=(t,l,a=new Set)=>((l={get value(){return e?.deps.push(a.add(e)),t},set value(e){t=e;for(let t of a)t(e)},peek:()=>t}).toJSON=l.then=l.toString=l.valueOf=()=>l.value,l),effect=(t,l,a,u)=>(u=(a=u=>{l?.call&&l(),u=e,e=a;try{l=t()}finally{e=u}}).deps=[],a(),e=>{for(;e=u.pop();)e.delete(a)}),computed=(e,t=signal(),l,a)=>((l={get value(){return a||=effect((()=>t.value=e())),t.value}}).toJSON=l.then=l.toString=l.valueOf=()=>l.value,l),batch=e=>e(),untracked=(t,l,a)=>(l=e,e=null,a=t(),e=l,a);

Add untracked function

Hi @kethan !

Nice philosophy, smallest size so far!
I'd suggest removing o / memo and adding untacked function (since you've removed peek) - all it needs is to run effect in zero context (example for usignal):

const untracked = (fn, prev) => (prev = current, current = null, fn(), current = prev)

Looking forward, thanks!
I would integrate it in sprae as direct dependency

Add tests

@kethan as suggested, here's tests:

import t, { is, throws } from 'tst'
import { signal, computed, effect } from './signal.js'

// value
t('signal: readme', async t => {
  let log = []
  let v1 = signal(0)
  is(v1.value, 0)

  // subscribe
  let unsub = effect(() => log.push(v1.value))

  // set
  v1.value = 1
  is(v1.value, 1)
  // is(log, [0, '-', 1])
  unsub()

  // from value
  let v2 = computed(() => v1 * 2)
  log = []
  effect(() => log.push(v2.value))
  is(log, [2])
  is(v2.value, 2) // > 2
  is(v1.value, 1)
  is(log, [2])
  console.log('v1.value = 2')
  v1.value = 2
  is(log, [2, 4])

  // initialize value
  let v3 = signal(v1)
  is(v3.value, v1) // v5

  // dispose
  // v2.dispose()
  // ;[v3, v2, v1].map(v => v[Symbol.dispose]())
})

t('signal: callstack trouble', t => {
  let v1 = signal(0)
  let v2 = computed(() => { console.log('v2.compute'); return v1.value })
  effect(() => { console.log('v2.subscribed'), v2.value })
  console.log('---- v1.value = 1')
  v1.value = 1
})

t('signal: core API', t => {
  // warmup
  let v1 = signal(0)
  let v2 = computed(() => v1 * 2)
  effect(() => (v2.value))
  v1.value = 2

  console.log('---start')
  let s = signal(0)
  let log = []
  effect(value => log.push(s.value))

  is(log, [0], 'should publish the initial state')

  is(+s, 0, 'toPrimitive')
  is(s.valueOf(), 0, 'valueOf')
  is(String(s.toString()), '0', 'toString')
  is(s.value, 0, 's()')


  s.value = 1
  is(+s, 1, 'state.current = value')

  s.value = 2
  is(+s, 2, 'state(value)')
  is(s.value, 2, 'state(value)')

  s.value += 1
  is(s.value, 3, 'state(state + value)')

  // observer 2
  let log2 = []
  effect(() => log2.push(s.value))

  is(log.slice(-1), [3], 'should track and notify first tick changes')
  is(log2, [3], 'should properly init set')
  s.value = 4
  is(log.slice(-1), [4], 'arbitrary change 1')
  s.value = 5
  is(log.slice(-1), [5], 'arbitrary change 2')
  is(log2.slice(-1), [5], 'secondary observer is fine')
})

t.skip('signal: should not expose technical/array symbols', async t => {
  let s = signal({ x: 1 })
  let log = []
  is(s.map, undefined)
  for (let p in s) { log.push(p) }
  is(log, [])
})

t('signal: multiple subscriptions should not inter-trigger', async t => {
  let value = signal(0)
  let log1 = [], log2 = [], log3 = []
  effect(v => log1.push(value.value))
  effect(v => log2.push(value.value))
  is(log1, [0])
  is(log2, [0])
  value.value = 1
  is(log1, [0, 1])
  is(log2, [0, 1])
  effect(v => log3.push(value.value))
  is(log1, [0, 1])
  is(log2, [0, 1])
  is(log3, [1])
  value.value = 2
  is(log1, [0, 1, 2])
  is(log2, [0, 1, 2])
  is(log3, [1, 2])
})

t('signal: stores arrays', async t => {
  let a = signal([])
  is(a.value, [])
  a.value = [1]
  is(a.value, [1])
  a.value = [1, 2]
  is(a.value, [1, 2])
  a.value = []
  is(a.value, [])

  let b = signal(0)
  a = signal([b])
  is(a.value, [b])
  b.value = 1
  is(a.value, [b])
  a.value = [b.value]
  is(a.value, [1])
})

t('signal: stringify', async t => {
  let v1 = signal(1), v2 = signal({ x: 1 }), v3 = signal([1, 2, 3])
  is(JSON.stringify(v1), '1')
  is(JSON.stringify(v2), `{"x":1}`)
  is(JSON.stringify(v3), '[1,2,3]')
})

t('signal: subscribe value', async t => {
  let v1 = signal(1), log = []
  effect(v => log.push(v1.value))
  is(log, [1])
  v1.value = 2
  is(log, [1, 2])
})

t('signal: internal effects', async t => {
  const s1 = signal(1), s2 = signal(2)
  let log1 = [], log2 = []

  effect(() => {
    log1.push(s1.value)
    return effect(() => {
      log2.push(s2.value)
    })
  })

  is(log1, [1]), is(log2, [2])
  s1.value++
  is(log1, [1, 2]), is(log2, [2, 2])
  s1.value++
  is(log1, [1, 2, 3]), is(log2, [2, 2, 2])

  s2.value++
  is(log1, [1, 2, 3]), is(log2, [2, 2, 2, 3])
})

// error
t.todo('signal: error in mapper', async t => {
  // NOTE: actually mb useful to have blocking error in mapper
  let x = signal(1)
  let y = x.map(x => { throw Error('123') })
  t.ok(y.error)
})
t.todo('signal: error in subscription', async t => {
  let x = signal(1)
  x.subscribe(() => { throw new Error('x') })
})
t.todo('signal: error in init', async t => {
  let x = signal(() => { throw Error(123) })
})
t.todo('signal: error in set', async t => {
  let x = signal(1)
  x(x => { throw Error(123) })
})

// effect
t('effect: single', t => {
  // NOTE: we don't init from anything. Use strui/from
  let log = [], v1 = signal(1)
  effect(() => log.push(v1.value))
  is(log, [1])
  v1.value = 2
  is(log, [1, 2])
})

t('effect: teardown', t => {
  const a = signal(0)
  const log = []
  let dispose = effect(() => {
    log.push('in', a.value)
    const val = a.value
    return () => log.push('out', val)
  })
  // is(log, [])
  // a.value = 0
  is(log, ['in', 0])
  a.value = 1
  is(log, ['in', 0, 'out', 0, 'in', 1])
  dispose()
  is(log, ['in', 0, 'out', 0, 'in', 1, 'out', 1])
})


// computed
t('computed: single', t => {
  let v1 = signal(1), v2 = computed(() => v1.value)
  is(v2.value, 1)
  v1.value = 2
  is(v2.value, 2)
})

t('computed: multiple', t => {
  let v1 = signal(1), v2 = signal(1), v3 = computed(() => v1.value + v2.value)
  is(v3.value, 2)
  v1.value = 2
  is(v3.value, 3)
  v2.value = 2
  is(v3.value, 4)
})

t('computed: chain', t => {
  let a = signal(1),
    b = computed(() => (console.log('b'), a.value + 1)),
    c = computed(() => (console.log('c'), b.value + 1))

  is(c.value, 3)
  a.value = 2
  is(c.value, 4)
  a.value = 3
  is(c.value, 5)
})

Sorry for not making a PR.
You can take any test runner to your flavor.
I'd suggest setting up a github action and bumping version to major.
Lmk if you need help with that.

Thanks @kethan

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.