Skip to content
Engine

State and Logic

State

Reactive values. Change a Value and everything computed from it, bound to it or watching it updates by itself.

PackageShared

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.

A health bar that updates itself
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 update

A 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)?): Value

A value you can read and write. Setting it to something equal to the current value (by ==, or your own equals) does nothing.

MemberDescription
:Get(): anyThe 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)?): Computed

A 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)?): Computed

Like 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: () -> ()): Effect

Runs 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()
MemberDescription
Active: booleanfalse after :Stop().
:Stop()Stops the effect from running again.

Array

State.Array(initial: { any }?, equals?): ArrayHandle

A reactive list. Every change replaces the stored table with a new one, so Changed hands you the new and the old list.

MemberDescription
: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).
ChangedFires with (newList, oldList).

Dict

State.Dict(initial: { [any]: any }?, equals?): DictHandle

A reactive dictionary with :Get(key), :GetAll(), :Count(), :Set(key, value), :Delete(key), :Clear(), :Update(fn) and Changed.

Constant

State.Constant(value: any): Constant

A value that never changes but can be used anywhere a reactive value is expected.

FromPromise

State.FromPromise(promise: Promise): Value

A 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?): History

Records 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.

MemberDescription
CanUndo: boolean, CanRedo: booleanRead-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): Computed

Shortcut for State.Computed(function() return fn(source:Get()) end).

All

State.All(sources: { [string]: Value | Computed }): Computed

A 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): any

Reads 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 }