Skip to content

loom-go/sig

Repository files navigation

sig

Reactive signals in Go

count := sig.NewSignal(0)

sig.NewEffect(func() {
    fmt.Println("changed", count.Read())
})

count.Write(10)

Features

  • Signals, effects, computed values (memos), contexts, batching, untrack, and owners
  • Automatic dependency tracking
  • Per-goroutine runtime isolation
  • Height-based priority scheduling
  • Topological ordering
  • Infinite loop detection
  • Staleness detection
  • Zero dependency

Coming soon:

  • Async computed values

Introduction

sig is based on the very latest from the SolidJS team (sou-rc-e-s). It aims to be a fully fledged signal-based reactive model with async first support, that can be embedded anywhere.

Note: sig is purpose built for framework authors to add reactivity in their tool. For a more user-friendly and SolidJS-like API, see loom/signals.

TODOs

☑️ signals
count := sig.NewSignal(0)
fmt.Println(count.Read())

count.Write(10)
fmt.Println(count.Read())

// Output
// 0
// 10
☑️ computed
count := sig.NewSignal(1)
double := sig.NewComputed(func() int {
    fmt.Println("doubling")
    return count.Read()*2
})
fmt.Println(count.Read())
fmt.Println(double.Read())

count.Write(10)
fmt.Println(count.Read())
fmt.Println(double.Read())

// Output:
// doubling
// 1
// 2
// doubling
// 10
// 20
☑️ effects
count := sig.NewSignal(1)
fmt.Println(count.Read())

sig.NewEffect(func() {
    fmt.Println(count.Read()*2)
})

count.Write(10)
fmt.Println(count.Read())

// Output:
// 1
// 2
// 20 -- note that effects run immediately on setCount(). this is different than Solid's reactive system (see Batch() for alternatives)
// 10
☑️ batch
count := sig.NewSignal(1)
fmt.Println(count.Read())

sig.NewEffect(func() {
    fmt.Println(count.Read()*2)
})

sig.NewBatch(func () {
    count.Write(10)
    fmt.Println(count.Read())
})

// Output:
// 1
// 2
// 10
// 20 -- now with batch, effects are defered to the end of the batch, so 10 is logged before 20.
//       batch can also be used to update a state multiple times while making sure its effects are only run once.
☑️ owner
// mainly used by framework authors to "own" a reactive context and dispose it when appropriate
owner := sig.NewOwner()
owner.OnError(func (err any) {
    fmt.Println("recovered:", err)
})

err := owner.Run(func() error {
    count := sig.NewSignal(1)
    fmt.Println(count.Read())

    sig.NewEffect(func() {
        fmt.Println(count.Read()*2)
        sig.OnCleanup(func() {
            fmt.Println("disposed")
        })
    })

    count.Write(10)
    fmt.Println(count.Read())

    return nil
})

owner.Dispose()

// Output:
// doubling
// 1
// 2
// disposed
// 20
// 10
// disposed
⬜ async computed
userID := sig.NewSignal(0)
user := sig.NewAsyncComputed(func() (User, error) { // func is called in a goroutine
    return getUser(userID.Read())
})

sig.NewEffect(func() {
    if sig.IsPending(user) { // uses the panic logic to know if the computed node has resolved yet or not
        fmt.Println("loading...")
        return nil
    }

    // if we're in a reactive scope and user has not resolved yet, this will panic and be recovered to tell the node one of its dependencies is not ready.
    // else it returns an error to avoid panics in a scope not owned by the reactive system.
    u, err := user.Read()
    if err {
        fmt.Println("error:", err)
        return nil
    }

    fmt.Println("user:", u.Name)
    return nil
})

// Output:
// loading...
// user: Bob
☑️ context
ctx := sig.NewContext("light") // default value

owner := sig.NewOwner()
owner.Run(func() error {
    ctx.Set("dark")

    sig.NewOwner().Run(func() error {
        theme := ctx.Get()
        fmt.Println(theme)

        return nil
    })

    return nil
})

theme := ctx.Get() // returns default value
fmt.Println(theme)

// Output:
// dark
// light
☑️ untrack
count := sig.NewSignal(1)
other := sig.NewSignal(10)

sig.NewEffect(func() {
    fmt.Println(count.Read(), sig.Untrack(other.Read))
})

count.Write(2)
other.Write(20)

// Output:
// 1, 10
// 2, 10 -- stops here and no effect is triggered for the setOther(20)

FAQ

Differences with SolidJS's reactive model

TODO: instant flush and batching, multi-threading for async computed, no need for async effects because you can just go fn() wherever to go async

Credits

  • Ryan Carniato, Milo Mighdoll, and everyone else pushing the limits of what's possible with reactive systems <3

About

Reactive signals in Go

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors