-
Notifications
You must be signed in to change notification settings - Fork 1
State Containers
local state = ennui.State()Pass a table to pre-populate state:
local state = ennui.State({
name = "Player",
health = 100,
position = { x = 0, y = 0 }
})Read and write reactive properties through state.props:
-- read
local hp = state.props.health
-- write (triggers watchers and computed)
state.props.health = state.props.health - 10bind() returns a cached Computed that tracks a property path. Subsequent calls with
the same path return the same instance:
local healthBinding = state:bind("health")
-- dot-notation for nested paths
local xBinding = state:bind("position.x")get() reads a value at a dot-notation path. The returned value is still a reactive
proxy when it is a table:
local x = state:get("position.x")getRaw() unwraps the top-level proxy. Nested tables inside the result may still be
proxies. Useful when you need a plain reference but don't care about deep nesting:
local rawTasks = state:getRaw("tasks")
-- rawTasks is a plain table, but rawTasks[1] may still be a proxygetRawDeep() returns a disconnected plain-Lua copy with all nested proxies unwrapped.
Use this for serialization, drag-and-drop data, rebuilding arrays etc. etc.:
local snapshot = state:getRawDeep("tasks")
-- snapshot is a plain Lua table with no reactivity at any levelscope() returns a StateScope that roots all paths at the given prefix:
local positionScope = state:scope("position")Inside a scope, paths are relative to the scope root. scope.props works like
state.props for the scoped object:
local positionScope = state:scope("position")
local x = positionScope.props.x
positionScope.props.y = positionScope.props.y + 1
-- dot-notation also works
local x2 = positionScope:get("x")bind() and format() on a scope use paths relative to the scope root:
local positionScope = state:scope("position")
local xBinding = positionScope:bind("x")
local positionLabel = positionScope:format("({x}, {y})")scope() can be called on an existing scope to nest further:
local worldScope = state:scope("world")
local playerScope = worldScope:scope("player")
-- playerScope roots at "world.player"State.newId() returns a unique string ID. Use it as a stable key for list items:
state.props.tasks[1] = { id = ennui.State.newId(), text = "Buy milk", done = false }This is particularly useful when using reactive list binding.
Nested objects inside arrays are also reactive proxies. Assign whole objects to replace them, or write individual fields to update in place:
-- replace the whole item
state.props.tasks[1] = { id = existingId, text = "Updated text", done = true }
-- update a single field
state.props.tasks[1].text = "Updated text"state:ipairs(path) is the reactive equivalent of ipairs. Use it inside a computedInline getter so the computed re-runs when the array changes:
local activeCount = state:computedInline(function()
local count = 0
for _, task in state:ipairs("tasks") do
if not task.done then
count = count + 1
end
end
return count
end)state:pairs(path) is the reactive equivalent of pairs for non-array tables:
local keyCount = state:computedInline(function()
local n = 0
for _ in state:pairs("config") do n = n + 1 end
return n
end)state:forEach(path, fn) calls fn(scope, index) for each element, where scope is a StateScope rooted at that element's path. Useful for imperative setup:
state:forEach("players", function(scope, i)
print(i, scope.props.name)
end)state:map(path, fn) collects the return values of fn(scope, index) into an array:
local names = state:map("players", function(scope)
return scope.props.name
end)Use :len() on a reactive array proxy - the # operator does not work correctly on
proxies:
local n = state.props.tasks:len()Call state:cleanup() to dispose all watchers and computed properties when the state
is no longer needed:
state:cleanup()The # operator does not work correctly on reactive proxies. Use :len() instead:
-- Wrong: may return 0 or stale length
local n = #state.props.tasks
-- Correct
local n = state.props.tasks:len()Mutating a table obtained via getRaw() bypasses the reactive proxy, so watchers and
computed properties will not fire:
-- Wrong: modifies the raw table directly, no updates triggered
local raw = state:getRaw("tasks")
raw[1] = { id = "x", text = "oops, bad, wrong" }
-- Correct: write through the proxy
state.props.tasks[1] = { id = "x", text = "good, correct, wonderful" }table.insert and table.remove operate on the underlying raw table and do not trigger reactive updates. Use direct index assignment instead:
-- Wrong
table.insert(state.props.tasks, { id = ennui.State.newId(), text = "Task" })
-- Correct: append via indexed assignment
local n = state.props.tasks:len()
state.props.tasks[n + 1] = { id = ennui.State.newId(), text = "Task" }
-- Correct: remove by rebuilding from getRawDeep and reassigning
local plain = state:getRawDeep("tasks")
table.remove(plain, index)
state.props.tasks = plain