DX-oriented observable table implementation for Luau
ReactiveTable wraps normal Luau tables in a proxy that behaves like a table, but automatically broadcasts changes It is designed to reduce the boilerplate of manual update calls while keeping the code ergonomic
- Table-like syntax via metatables (
t.key,t.key = value,for k, v in t do ... end) - Signals for reactivity:
Changed(fires on value updates)New(fires on key additions)Removed(fires on key removals)
- Automatic wrapping of nested tables into ReactiveTable objects
- 1-level ancestor notification (child writes bubble to direct parent only)
- Freeze controls:
- freeze the table (rejects writes)
- freeze signals (suppresses signal emissions)
- Utility methods:
Clone,Compare,Find,Sort,Insert,Remove,Clear - Supports self-referential tables during construction (no stack overflow)
Add ReactiveTable to your wally.toml:
[dependencies]
ReactiveTable = "elentium/reactivetable@0.1.1"Then run:
wally install- Install roblox/ReactiveTable.rbxm
- Insert it in your roblox studio project
local ReactiveTable = require(path.to.module) -- see Installation notes
local reactive = ReactiveTable.new({ hello = "world" })
print(reactive.hello) -- "world"
reactive:BindToKeyChanged("hello", function(newValue: any)
print(`key "hello" changed, new value: {newValue}`)
end)
reactive.hello = "goodbye" -- triggers the callback
-- listen for new keys
reactive.New:Connect(function(newKey: any, newValue: any)
print(`New key added: {newKey}, value: {newValue}`)
end)
reactive.someNewKey = 123 -- triggers Newlocal ReactiveTable = require(path.to.module)
local reactive = ReactiveTable.new({ a = { b = 10 } })
reactive.Changed:Connect(function(key: string, value: any)
print(`changed {key} -> {value}`)
end)
-- This fires `Changed` on the `a` table (and also on `reactive`)
reactive.a.b = 11ReactiveTable exposes three signal objects:
reactive.Changed:Connect(function(key, value) ... end)reactive.New:Connect(function(key, value) ... end)reactive.Removed:Connect(function(key) ... end)
BindToKeyChanged(key, callback) is a convenience wrapper for Changed that filters by a specific key.
Connections returned by :Connect(...) support :Disconnect().
Creates a ReactiveTable proxy.
tbl?is an optional source table (defaults to{}).- Nested tables inside
tblare automatically wrapped into ReactiveTable objects. - If the same raw table is wrapped more than once, the same userdata wrapper is reused (per process lifetime).
reactive:GetSource() -> { [any]: any }reactive:GetSize() -> numberreactive:IsFrozen() -> booleanreactive:AreSignalsFrozen() -> boolean
reactive:Destroy()- Disconnects all signal listeners
- Removes internal registry entries
reactive:BindToKeyChanged(key, callback) -> SignalConnectioncallback(newValue)
reactive:BroadcastChanges(changedKeys: { [any]: "Added" | "Removed" | "Changed" })- Manually emits signals for multiple keys (useful for batch updates)
reactive:Insert(value: any, index?: number) -> ()- Array-style insertion with
table.insert - Emits
Newand bubblesChangedto direct ancestors
- Array-style insertion with
reactive:Remove(index?: number) -> any- Array-style removal with
table.remove - Emits
Removedand bubblesChangedto direct ancestors
- Array-style removal with
reactive:Find(needle: any, init?: number) -> number?- Works with primitives and ReactiveTable userdata.
reactive:Sort(comparator) -> ()- Sorts array contents via
table.sort.
- Sorts array contents via
reactive:Clear() -> ()- Clears an array-like ReactiveTable (sets numeric entries to nil)
reactive:Clone(deepClone?: boolean) -> ReactiveTable<T>deepClone == truedeep-copies nested tables (and clones nested reactive children)deepClone == nil/falsedoes a shallow copy: top-level is independent, nested tables are shared
reactive:Compare(other: any) -> boolean- Deep-compares contents
reactive:Freeze() / reactive:Unfreeze()- Rejects write operations when frozen
reactive:FreezeSignals() / reactive:UnfreezeSignals()- Suppresses signal emissions for that specific object
- Luau autocomplete for nested tables:
- Autocomplete will show table contents, but will not show ReactiveTable API for nested objects unless you annotate types (see
src/init.luauheader docs). - Ancestor notifications are limited to 1-level for performance and simplicity.
The project includes a large test suite at src/test/init.luau.
Requiring it in a Roblox environment runs correctness + performance checks and prints results/optionally creates a UI.
Contributions are welcome. If you add features or fix edge cases, consider adding/adjusting tests in src/test/init.luau.
Apache License 2.0. See LICENSE for details.
IAMNOTULTRA3 (a.k.a. Elite, Elentium)
Signal batching implementation based on stravant (Signal) and modified by IAMNOTULTRA3.