Appearance
State Management (Hooks)
Vulpis implements a reactive state system similar to React's useState, but with a key difference: State keys are global strings.
When you update state, the engine automatically detects the change, re-runs your App() function to generate a new Virtual DOM, and efficiently updates only the parts of the UI that changed (Reconciliation).
API Reference
useState(key, defaultValue)
Retrieves the current value for a given state key. If the key does not exist yet, it initializes it with the defaultValue.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | A unique identifier for this state variable. |
defaultValue | any | No | The initial value (number, string, or boolean) if the state hasn't been set yet. |
Returns:
- The current value of the state (or the default value).
Example:
lua
local count = useState("app_counter", 0)
local username = useState("user_name", "Guest")setState(key, newValue)
Updates the value of a state key and triggers a re-render of the UI.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The unique identifier of the state to update. |
newValue | any | Yes | The new value to store. Supports numbers, strings, and booleans. |
Behavior: Calling setState marks the internal State Manager as "dirty". On the next frame tick, the engine will re-execute your Lua App function and reconcile the changes to the screen.
vulpis.markDirty()
Safely forces the internal engine to register a state change and trigger a UI re-render, even if setState was not explicitly called.
When to use it: Use this function when you are bypassing the setState API to update custom global Lua tables. Because the C++ engine cannot automatically detect changes inside standard Lua tables, markDirty() acts as a manual override to tell the engine that it needs to rebuild the VDOM on the next tick.
Examples
1. Simple Counter
A button that increments a number when clicked.
lua
local el = require("utils.core.elements")
-- Define global state keys to avoid typos
local KEY_COUNT = "counter_val"
function App()
-- 1. Retrieve current state (defaulting to 0)
local count = useState(KEY_COUNT, 0)
return el.VBox({
style = {
justifyContent = "center",
alignItems = "center",
gap = 20
},
children = {
-- Display the count
el.Text({
text = "Count: " .. tostring(count),
style = { fontSize = 32 }
}),
-- Button to increment
el.VBox({
style = {
BGColor = "#007ACC",
padding = 10
},
onClick = function()
-- 2. Update state (triggers re-render)
setState(KEY_COUNT, count + 1)
end,
children = {
el.Text({ text = "Increment", style = { color = "#FFF" } })
}
})
}
})
end2. Toggle Switch
A boolean state to toggle visibility or appearance.
lua
function App()
local isOn = useState("toggle_switch", false)
return el.VBox({
onClick = function()
setState("toggle_switch", not isOn)
end,
style = {
w = 100, h = 50,
BGColor = isOn and "#00FF00" or "#FF0000", -- Conditional Styling
justifyContent = "center",
alignItems = "center"
},
children = {
el.Text(isOn and "ON" or "OFF")
}
})
end3. Manual Re-renders with markDirty
Managing state in a custom global table without using setState.
lua
local el = require("utils.core.elements")
-- A custom global state table
local GameState = { score = 0 }
function App()
return el.VBox({
children = {
el.Text("Score: " .. GameState.score),
el.Button({
text = "Add Points",
onClick = function()
-- Update the Lua table directly
GameState.score = GameState.score + 10
-- Tell Vulpis to redraw the UI to reflect the new score
vulpis.markDirty()
end
})
}
})
endImportant: Global Namespace
Unlike React, where state is isolated per-component instance, Vulpis state is stored in a global map accessed by string keys.
The Implications:
- Shared State: If two different components use
useState("count"), they will share the exact same value. Updating one will update the other. - Naming Convention: To avoid conflicts, it is best practice to namespace your keys.
useState("active")(Too generic)useState("navbar_active_tab")useState("settings_volume_level")
lua
-- Good Practice: Unique keys for distinct components
local function CounterA()
local val = useState("counter_A", 0)
-- ...
end
local function CounterB()
local val = useState("counter_B", 0) -- Different key = Independent state
-- ...
end