Scalable and minimal reactive programming framework for Luau.
Signals provides fine-grained reactivity through a minimal set of primitives that automatically track dependencies and efficiently propagate updates. It enables building reactive systems where only the necessary computations re-run when data changes.
Those with limited experience in reactive programming may find this article helpful as a pre-read.
The initial motivation for this library originated with the desire for a performant state management solution. In particular, we sought to move from a global "Redux-like" state towards a distributed "fine-grained" state graph.
The core API is very minimal, consisting of three core primitives:
createSignal<T>(initial: (() -> T) | T, equals: equals<T>?): (getter<T>, setter<T>)Creates a queryable and settable value.
- Lazy-initializable with a constructor
- Cacheable with an optional
equalsparameter - The getter can be provided a
scopefor automatic dependency tracking (see createComputed and createEffect)
local getFirstName, setFirstName = createSignal("David")
local getLastName, setLastName = createSignal("Tennant")
print(getFirstName(false)) -- prints: David
setFirstName("The")
setLastName("Doctor")
print(`{getFirstName(false)} {getLastName(false)}`) -- prints: The DoctorcreateComputed<T>(computed: (scope) -> T, equals: equals<T>?): getter<T>Creates a read-only reactive derived value.
- Lazy evaluation (computed updates when value is read)
- Can be used to define "derived" state using signals and other computeds
- The
scopecan be used to automatically and reactively track updates to dependencies
local getFullName = createComputed(function(scope)
return `{getFirstName(scope)} {getLastName(scope)}`
end)
print(getFullName(false)) -- prints: The DoctorcreateEffect(effect: (scope) -> ()): disposeCreates a reactive side effect.
- Eager evaluation
- The
scopecan be used to automatically and reactively track updates to dependencies
local dispose = createEffect(function(scope)
print(`Their real name is {getFullName(scope)}`)
end)
-- prints: Their real name is The Doctor
batch(function()
setFirstName("David")
setLastName("Tennant")
end)
-- prints: Their real name is David Tennant
dispose()Warning
You MUST store a strong reference to the dispose function returned from createEffect for the effect to be guaranteed to re-run. Not storing a strong reference to this function means the effect is liable to be garbage collected.
The reactive graph uses a pull-based lazy evaluation model with automatic dependency tracking via the scope mechanism. When a getter is called with a scope, the source registers itself with the observing computed or effect. Updates propagate through the graph and are coalesced via the scheduler to avoid redundant recomputation.
For more detail on the algorithms, see:
- Reactive Algorithms (Reactively)
- Monotonic Painting
See CONTRIBUTING.md for development setup, coding standards, and how to submit changes.
This library builds upon the existing work on reactive programming, particularly drawing inspiration from S.js, Reactively, Fusion, and jotai.
This project is licensed under the terms of the MIT license.