Reactive signals in Go
count, setCount := sig.Signal(0)
sig.Effect(func() func() {
fmt.Println("changed", count())
return nil
})
setCount(10)- Signals, effects, computed values (memos), 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:
- Contexts
- 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 system with async first support, that can be embedded anywhere.
☑️ signals
count, setCount := sig.Signal(0)
fmt.Println(count())
setCount(10)
fmt.Println(count())
// Output
// 0
// 10☑️ computed
count, setCount := sig.Signal(1)
double := sig.Computed(func() int {
fmt.Println("doubling")
return count()*2
})
fmt.Println(count())
fmt.Println(double())
setCount(10)
fmt.Println(count())
fmt.Println(double())
// Output:
// doubling
// 1
// 2
// doubling
// 10
// 20☑️ effects
count, setCount := sig.Signal(1)
fmt.Println(count())
sig.Effect(func() func() {
fmt.Println(count()*2)
return nil
})
setCount(10)
fmt.Println(count())
// 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, setCount := sig.Signal(1)
fmt.Println(count())
sig.Effect(func() func() {
fmt.Println(count()*2)
return nil
})
sig.Batch(func () {
setCount(10)
fmt.Println(count())
})
// 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.Owner()
owner.OnError(func (err error) {
fmt.Println("recovered:", err)
})
owner.Run(func() {
count, setCount := sig.Signal(1)
fmt.Println(count())
sig.Effect(func() func() {
fmt.Println(count()*2)
return func() {
fmt.Println("disposed")
}
})
setCount(10)
fmt.Println(count())
})
owner.Dispose()
// Output:
// doubling
// 1
// 2
// disposed
// 20
// 10
// disposed⬜ async computed
userID, setUserID := sig.Signal(0)
user := sig.AsyncComputed(func() (User, error) { // func is called in a goroutine
return getUser(userID())
})
sig.Effect(func() 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()
if err {
fmt.Println("error:", err)
return nil
}
fmt.Println("user:", u.Name)
return nil
})
// Output:
// loading...
// user: Bob⬜ context
ctx := sig.Context("light") // default value
owner := sig.Owner()
owner.Run(func() {
sig.SetContext[string](ctx, "dark")
sig.Owner().Run(func() {
theme := sig.GetContext[string](ctx)
fmt.Println(theme)
})
})
theme := sig.GetContext[string](ctx) // returns default value
fmt.Println(theme)
// Output:
// dark
// light☑️ untrack
count, setCount := sig.Signal(1)
other, setOther := sig.Signal(10)
sig.Effect(func() func() {
fmt.Println(count(), sig.Untrack(other))
return nil
})
setCount(2)
setOther(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 at SolidJS for pushing the limits of what's possible with reactive systems.