Reactive signals in Go
count := sig.NewSignal(0)
sig.NewEffect(func() {
fmt.Println("changed", count.Read())
})
count.Write(10)- 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
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:
sigis purpose built for framework authors to add reactivity in their tool. For a more user-friendly and SolidJS-like API, see loom/signals.
☑️ 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)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
- Ryan Carniato, Milo Mighdoll, and everyone else pushing the limits of what's possible with reactive systems <3