State and Logic
State
Reactive values. Change a Value and everything computed from it, bound to it or watching it updates by itself.
Getting it
local State = Engine:GetPackage("State")Overview
State is a small reactive system. You keep data in Values, describe derived data as Computed functions, and react to changes with Effects or by binding a value to an instance property. When a Value changes, only the things that depend on it recompute, each exactly once and in the right order.
local State = Engine:GetPackage("State")
local health = State.Value(100)
local maxHealth = State.Value(100)
local fraction = State.Computed(function()
return health:Get() / maxHealth:Get()
end)
-- Keep the bar's size in sync with the fraction
fraction.Changed:Connect(function(value)
bar.Size = UDim2.fromScale(value, 1)
end)
-- Or bind a computed straight to a property
local label = State.Computed(function()
return string.format("%d / %d", health:Get(), maxHealth:Get())
end)
label:Bind(textLabel, "Text")
health:Set(75) -- fraction and label both updateA Computed tracks which Values it read during its last run. You do not list dependencies yourself (use Derive if you want to).
Batching
Setting several Values one after the other recomputes dependents after each set. Wrap the sets in State.Batch to recompute once at the end:
State.Batch(function()
health:Set(50)
maxHealth:Set(200)
end)Creating state
Value
State.Value(initial: any, equals: ((a, b) -> boolean)?): ValueA value you can read and write. Setting it to something equal to the current value (by ==, or your own equals) does nothing.
| Member | Description |
|---|---|
:Get(): any | The current value. Inside a Computed or Effect this also records the dependency. |
:Set(value) | Replaces the value, fires Changed and updates dependents. |
:Update(fn) | :Set(fn(current)). |
:Bind(instance, property): () -> () | Writes the value to instance[property] now and on every change. Returns a function that stops the binding. The binding also stops when the instance is destroyed. |
Changed: Signal<(new, old)> | Fires after the value changes. |
Computed
State.Computed(fn: () -> any, equals: ((a, b) -> boolean)?): ComputedA read-only value calculated by fn. It runs once immediately and again whenever one of the values it read changes. It has :Get(), :Bind() and Changed, and fires Changed only when its result actually changes. A Computed that reads itself, or a loop between Computeds, raises an error.
Derive
State.Derive(dependencies: { Value | Computed }, fn: () -> any, equals: ((a, b) -> boolean)?): ComputedLike Computed, but it depends on exactly the listed values instead of tracking reads. Use it when fn reads values conditionally and you want it to update on all of them regardless.
Effect
State.Effect(fn: () -> ()): EffectRuns fn now and again whenever a value it read changes. Use it for side effects that are not a single property binding.
local effect = State.Effect(function()
if health:Get() <= 0 then
showDeathScreen()
end
end)
-- later
effect:Stop()| Member | Description |
|---|---|
Active: boolean | false after :Stop(). |
:Stop() | Stops the effect from running again. |
Array
State.Array(initial: { any }?, equals?): ArrayHandleA reactive list. Every change replaces the stored table with a new one, so Changed hands you the new and the old list.
| Member | Description |
|---|---|
:Get(index), :GetAll(), :Count() | Read one item, a copy of the list, or its length. |
:Push(value), :Pop() | Add to or remove from the end. |
:Insert(index, value), :RemoveAt(index) | Insert or remove at a position. |
:Set(index, value), :Clear() | Replace one item, or empty the list. |
:Update(fn) | Replace the list with fn(copy). |
Changed | Fires with (newList, oldList). |
Dict
State.Dict(initial: { [any]: any }?, equals?): DictHandleA reactive dictionary with :Get(key), :GetAll(), :Count(), :Set(key, value), :Delete(key), :Clear(), :Update(fn) and Changed.
Constant
State.Constant(value: any): ConstantA value that never changes but can be used anywhere a reactive value is expected.
FromPromise
State.FromPromise(promise: Promise): ValueA Value that holds { Status, Value, Error }. It starts as { Status = "Pending" } and switches to "Resolved" with the value or "Rejected" with the error when the Promise settles.
local inventory = State.FromPromise(loadInventoryAsync())
State.Effect(function()
local state = inventory:Get()
loadingSpinner.Visible = state.Status == "Pending"
end)History
State.History(value: Value, maxEntries: number?): HistoryRecords every change of value so you can undo and redo. Keeps the last maxEntries changes (default 50). A new change after an undo clears the redo list.
| Member | Description |
|---|---|
CanUndo: boolean, CanRedo: boolean | Read-only. |
:Undo(), :Redo() | Step back or forward. |
:Clear() | Forget the history. |
Helpers
Observe / Once
State.Observe(source, fn: (new, old) -> ()): () -> ()
State.Once(source, fn: (new, old) -> ()): () -> ()Call fn on every change (or only the next change) of any reactive value. Return a function that stops listening.
Map
State.Map(source, fn: (value) -> any): ComputedShortcut for State.Computed(function() return fn(source:Get()) end).
All
State.All(sources: { [string]: Value | Computed }): ComputedA Computed table with the current value of every named source.
Filter
State.Filter(source, predicate: (value) -> boolean, initial: any?): (Value, () -> ())A Value that follows source but only takes values for which predicate returns true. Returns the value and a function that stops following.
Debounce / Throttle
State.Debounce(source, seconds: number, options?): (Value, () -> ())
State.Throttle(source, seconds: number, options?): (Value, () -> ())A Value that follows source, but debounced or throttled. Options are the same as RateLimiter.Debounce and RateLimiter.Throttle.
BindTwoWay
State.BindTwoWay(value: Value, instance: Instance, property: string): () -> ()Keeps a Value and an instance property in sync in both directions, for example a TextBox.Text that edits a Value and also shows changes made in code.
Batch
State.Batch(fn: () -> ())Runs fn and holds back Changed events and recomputation until it returns. Values that end up equal to where they started do not fire at all. Batches can be nested; errors inside fn are re-raised after the batch is flushed.
Peek
State.Peek(source): anyReads the current value without recording a dependency, so a Computed that peeks does not rerun when that value changes.
IsReactive / GetDependencies / GetDependents
State.IsReactive(value: any): boolean
State.GetDependencies(node): { any }
State.GetDependents(node): { any }Inspect the dependency graph, mostly useful when debugging.
Types
export type Value
export type Computed
export type Effect
export type ArrayHandle
export type DictHandle
export type Constant
export type History
export type AsyncStatus = "Pending" | "Resolved" | "Rejected"
export type AsyncState = { Status: AsyncStatus, Value: any, Error: any }