Skip to content

Roblox/signals

Repository files navigation

signals

Scalable and minimal reactive programming framework for Luau.

analyze tests

Overview

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.

Motivation

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.

Usage

The core API is very minimal, consisting of three core primitives:

createSignal

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 equals parameter
  • The getter can be provided a scope for 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 Doctor

createComputed

createComputed<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 scope can 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 Doctor

createEffect

createEffect(effect: (scope) -> ()): dispose

Creates a reactive side effect.

  • Eager evaluation
  • The scope can 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.

Implementation

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:

Contributing

See CONTRIBUTING.md for development setup, coding standards, and how to submit changes.

Acknowledgements

This library builds upon the existing work on reactive programming, particularly drawing inspiration from S.js, Reactively, Fusion, and jotai.

License

This project is licensed under the terms of the MIT license.

About

A declarative state management library for Luau.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages