State Management Basics
Master Rex's enhanced state management system with universal reactivity and helper methods
State is at the heart of any interactive application. Rex provides a powerful, reactive state management system with universal reactivity and enhanced helpers that make building dynamic UIs simple and predictable. This guide will teach you everything you need to know about managing state in Rex.
What is State?
State represents data that can change over time in your application. Examples include:
- User input (text in a form field)
- UI state (whether a modal is open)
- Application data (list of items, user preferences)
- Loading states (is data being fetched?)
Rex makes state reactive with universal auto-conversion - when state changes, your UI automatically updates and values are intelligently converted to the right types.
Creating State with useState
The most basic way to create state in Rex is with useState
:
local Rex = require(path.to.Rex)
local function Counter()
-- Create state with initial value of 0
local count = Rex.useState(0)
return Rex("Frame") {
children = {
Rex("TextLabel") {
Text = count,
Size = UDim2.fromScale(1, 0.5)
},
Rex("TextButton") {
Text = "Increment",
Size = UDim2.fromScale(1, 0.5),
Position = UDim2.fromScale(0, 0.5),
onClick = function()
count:increment()
end
}
}
}
end
Enhanced State Operation Helpers
Rex now includes powerful helper methods that eliminate boilerplate:
Numeric State Helpers
local count = Rex.useState(0)
-- Instead of: count:update(function(c) return c + 1 end)
count:increment() -- Add 1
count:increment(5) -- Add 5
count:decrement() -- Subtract 1
count:decrement(3) -- Subtract 3
Boolean State Helpers
local isVisible = Rex.useState(true)
-- Instead of: isVisible:update(function(v) return not v end)
isVisible:toggle() -- Flip the boolean value
Array State Helpers
local items = Rex.useState({"apple", "banana"})
-- Adding items
items:push("cherry") -- Add one item
items:push("date", "elderberry") -- Add multiple items
-- Removing items
local removed = items:pop() -- Remove and return last item
items:removeAt(2) -- Remove item at index 2
items:remove("banana") -- Remove by value
items:clear() -- Remove all items
Object State Helpers (Path Operations)
local user = Rex.useState({
name = "Player",
settings = {
theme = "dark",
volume = 0.8
}
})
-- Instead of complex nested updates
user:setPath("settings.theme", "light") -- Set nested property
user:setPath("settings.volume", 0.5)
-- Get nested values
local theme = user:getPath("settings.theme") -- Returns "light"
local volume = user:getPath("settings.volume") -- Returns 0.5
State Methods
Every state object has several methods:
state:get()
- Get the current valuestate:set(newValue)
- Set a new valuestate:update(function)
- Update based on current valuestate:map(function)
- Create a computed valuestate:each(function)
- Reactive list rendering for arrays
Practical Example: Task Manager
Here’s a complete example combining state helpers with reactive list rendering:
local function TaskManager()
local tasks = Rex.useState({
{id = 1, text = "Learn Rex", completed = false},
{id = 2, text = "Build UI", completed = false}
})
local newTaskText = Rex.useState("")
local nextId = Rex.useState(3)
local function addTask()
local text = newTaskText:get():gsub("^%s+", ""):gsub("%s+$", "") -- Trim whitespace
if text ~= "" then
tasks:push({
id = nextId:get(),
text = text,
completed = false
})
nextId:increment() -- Use increment helper
newTaskText:set("") -- Clear input
end
end
local function toggleTask(taskId)
tasks:update(function(currentTasks)
local newTasks = table.clone(currentTasks)
for i, task in ipairs(newTasks) do
if task.id == taskId then
newTasks[i].completed = not newTasks[i].completed
break
end
end
return newTasks
end)
end
local function removeTask(taskId)
tasks:update(function(currentTasks)
local newTasks = {}
for _, task in ipairs(currentTasks) do
if task.id ~= taskId then
table.insert(newTasks, task)
end
end
return newTasks
end)
end
return Rex("Frame") {
Size = UDim2.fromOffset(400, 500),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Color3.fromRGB(40, 44, 52),
children = {
Rex("UIListLayout") { Padding = UDim.new(0, 10) },
Rex("UIPadding") {
PaddingTop = UDim.new(0, 20),
PaddingLeft = UDim.new(0, 20),
PaddingRight = UDim.new(0, 20),
PaddingBottom = UDim.new(0, 20)
},
-- Title with task count
Rex("TextLabel") {
Text = tasks:map(function(taskList)
local completed = 0
for _, task in ipairs(taskList) do
if task.completed then completed = completed + 1 end
end
return `Tasks: {completed}/{#taskList} completed`
end),
Size = UDim2.new(1, 0, 0, 40),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextScaled = true,
LayoutOrder = 1
},
-- Add task input
Rex("Frame") {
Size = UDim2.new(1, 0, 0, 40),
BackgroundTransparency = 1,
LayoutOrder = 2,
children = {
Rex("UIListLayout") {
FillDirection = Enum.FillDirection.Horizontal,
Padding = UDim.new(0, 10)
},
Rex("TextBox") {
Text = newTaskText,
PlaceholderText = "Enter new task...",
Size = UDim2.new(0.8, 0, 1, 0),
BackgroundColor3 = Color3.fromRGB(60, 64, 72),
TextColor3 = Color3.new(1, 1, 1),
onTextChanged = function(textBox)
newTaskText:set(textBox.Text)
end,
LayoutOrder = 1
},
Rex("TextButton") {
Text = "Add",
Size = UDim2.new(0.2, 0, 1, 0),
BackgroundColor3 = Color3.fromRGB(67, 181, 129),
TextColor3 = Color3.new(1, 1, 1),
onClick = addTask,
LayoutOrder = 2
}
}
},
-- Task list using :each() method
Rex("ScrollingFrame") {
Size = UDim2.new(1, 0, 1, -100),
BackgroundTransparency = 1,
LayoutOrder = 3,
children = {
Rex("UIListLayout") { Padding = UDim.new(0, 5) },
tasks:each(function(task, index)
return Rex("Frame") {
Size = UDim2.new(1, 0, 0, 50),
BackgroundColor3 = task.completed
and Color3.fromRGB(67, 181, 129) -- Green when completed
or Color3.fromRGB(60, 64, 72), -- Gray when pending
key = tostring(task.id), -- Use stable ID as key
children = {
Rex("UIListLayout") {
FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 10)
},
-- Task text
Rex("TextLabel") {
Text = task.text,
Size = UDim2.new(0.6, 0, 1, 0),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextStrikethrough = task.completed,
TextXAlignment = Enum.TextXAlignment.Left,
LayoutOrder = 1
},
-- Toggle button
Rex("TextButton") {
Text = task.completed and "↶" or "✓",
Size = UDim2.new(0.2, 0, 0.8, 0),
BackgroundColor3 = task.completed
and Color3.fromRGB(255, 200, 100)
or Color3.fromRGB(100, 200, 255),
onClick = function()
toggleTask(task.id)
end,
LayoutOrder = 2
},
-- Remove button
Rex("TextButton") {
Text = "✕",
Size = UDim2.new(0.2, 0, 0.8, 0),
BackgroundColor3 = Color3.fromRGB(231, 76, 60),
TextColor3 = Color3.new(1, 1, 1),
onClick = function()
removeTask(task.id)
end,
LayoutOrder = 3
}
}
}
end)
}
}
}
}
end
This example demonstrates:
- Array helpers:
push()
to add tasks,increment()
for ID management - Reactive lists:
tasks:each()
for dynamic task rendering - State mapping:
tasks:map()
for computed task count display - Proper keys: Using
task.id
for efficient list updates - Interactive state: Toggle and remove operations with state updates
Reactive Properties
Instead of manually reading state with :get()
, you can make properties reactive:
local function ColorChanger()
local color = Rex.useState(Color3.fromRGB(255, 0, 0))
return Rex("Frame") {
-- Reactive property - updates automatically when color changes
BackgroundColor3 = color,
Size = UDim2.fromScale(1, 1),
children = {
Rex("TextButton") {
Text = "Change Color",
onClick = function()
-- Generate random color
local r = math.random(0, 255)
local g = math.random(0, 255)
local b = math.random(0, 255)
color:set(Color3.fromRGB(r, g, b))
end
}
}
}
end
Updating State Safely
Always use state methods to update state, never modify the value directly:
-- ❌ Wrong - Don't do this
local items = Rex.useState({"apple", "banana"})
table.insert(items:get(), "orange") -- This won't trigger updates!
-- ✅ Correct - Use :update() for safe mutations
local items = Rex.useState({"apple", "banana"})
items:update(function(currentItems)
local newItems = {}
for i, item in ipairs(currentItems) do
table.insert(newItems, item)
end
table.insert(newItems, "orange")
return newItems
end)
-- ✅ Or use :set() with a new array
items:set({"apple", "banana", "orange"})
Computed State with useComputed
Sometimes you need values derived from other state. Use useComputed
for this:
local function ShoppingCart()
local items = Rex.useState({
{name = "Apple", price = 1.50, quantity = 2},
{name = "Banana", price = 0.75, quantity = 3}
})
-- Computed value that recalculates when items change
local total = Rex.useComputed(function()
local sum = 0
for _, item in ipairs(items:get()) do
sum = sum + (item.price * item.quantity)
end
return sum
end, {items}) -- Dependencies
return Rex("Frame") {
children = {
Rex("TextLabel") {
Text = "Total: $" .. string.format("%.2f", total:get()),
Size = UDim2.fromScale(1, 0.2)
},
-- Item list would go here...
}
}
end
Auto-Tracked Computed
For simpler cases, use useAutoComputed
which automatically tracks dependencies:
local function TemperatureConverter()
local celsius = Rex.useState(0)
-- Auto-tracks celsius dependency
local fahrenheit = Rex.useAutoComputed(function()
return celsius:get() * 9/5 + 32
end)
return Rex("Frame") {
children = {
Rex("TextBox") {
PlaceholderText = "Celsius",
onTextChanged = function(instance)
local value = tonumber(instance.Text) or 0
celsius:set(value)
end
},
Rex("TextLabel") {
Text = "Fahrenheit: " .. tostring(fahrenheit:get())
}
}
}
end
Working with Complex State
For objects and arrays, Rex provides useDeepState
for nested reactivity:
local function UserProfile()
local user = Rex.useDeepState({
name = "John Doe",
email = "[email protected]",
preferences = {
theme = "dark",
notifications = true
}
})
return Rex("Frame") {
children = {
Rex("TextBox") {
Text = user.name, -- Reactive to name changes
onTextChanged = function(instance)
user.name:set(instance.Text)
end
},
Rex("TextButton") {
Text = "Toggle Theme",
onClick = function()
-- Access nested state
local currentTheme = user.preferences.theme:get()
local newTheme = currentTheme == "dark" and "light" or "dark"
user.preferences.theme:set(newTheme)
end
},
Rex("TextLabel") {
Text = "Theme: " .. user.preferences.theme:get()
}
}
}
end
State Best Practices
1. Keep State Minimal
Only store what needs to be reactive:
-- ❌ Don't store computed values in state
local function Component()
local firstName = Rex.useState("John")
local lastName = Rex.useState("Doe")
local fullName = Rex.useState("John Doe") -- Unnecessary!
-- Update fullName whenever first/last changes...
end
-- ✅ Use computed values instead
local function Component()
local firstName = Rex.useState("John")
local lastName = Rex.useState("Doe")
local fullName = Rex.useComputed(function()
return firstName:get() .. " " .. lastName:get()
end, {firstName, lastName})
end
2. Use the Right State Type
Choose the appropriate state primitive:
-- Simple values
local count = Rex.useState(0)
local message = Rex.useState("Hello")
-- Objects that need nested reactivity
local user = Rex.useDeepState({name = "John", age = 30})
-- Computed values
local displayName = Rex.useComputed(function()
return user.name:get() .. " (" .. tostring(user.age:get()) .. ")"
end, {user.name, user.age})
-- Auto-tracked computed (simpler syntax)
local isAdult = Rex.useAutoComputed(function()
return user.age:get() >= 18
end)
3. Name State Descriptively
Good state names make code self-documenting:
-- ❌ Unclear names
local s = Rex.useState(false)
local data = Rex.useState({})
-- ✅ Clear, descriptive names
local isModalOpen = Rex.useState(false)
local userProfiles = Rex.useState({})
local selectedUserId = Rex.useState(nil)
4. Group Related State
Keep related state together:
-- ❌ Scattered state
local isLoading = Rex.useState(false)
local error = Rex.useState(nil)
local data = Rex.useState(nil)
-- ✅ Grouped state
local apiState = Rex.useDeepState({
isLoading = false,
error = nil,
data = nil
})
-- Or use a custom hook
local function useApiCall()
local state = Rex.useDeepState({
isLoading = false,
error = nil,
data = nil
})
local fetchData = function()
state.isLoading:set(true)
-- Fetch logic...
end
return state, fetchData
end
Common State Patterns
Toggle Pattern
local function ToggleButton()
local isOn = Rex.useState(false)
return Rex("TextButton") {
Text = isOn:map(function(on) return on and "ON" or "OFF" end),
BackgroundColor3 = isOn:map(function(on)
return on and Color3.fromRGB(0, 255, 0) or Color3.fromRGB(255, 0, 0)
end),
onClick = function()
isOn:update(function(current) return not current end)
end
}
end
Counter Pattern
local function Counter()
local count = Rex.useState(0)
return Rex("Frame") {
children = {
Rex("TextButton") {
Text = "-",
onClick = function()
count:update(function(current) return math.max(0, current - 1) end)
end
},
Rex("TextLabel") {
Text = tostring(count:get())
},
Rex("TextButton") {
Text = "+",
onClick = function()
count:update(function(current) return current + 1 end)
end
}
}
}
end
Form Input Pattern
local function ContactForm()
local formData = Rex.useDeepState({
name = "",
email = "",
message = ""
})
local isValid = Rex.useComputed(function()
local data = {
name = formData.name:get(),
email = formData.email:get(),
message = formData.message:get()
}
return data.name ~= "" and data.email:match("@") and data.message ~= ""
end, {formData.name, formData.email, formData.message})
return Rex("Frame") {
children = {
Rex("TextBox") {
PlaceholderText = "Name",
Text = formData.name,
onTextChanged = function(instance)
formData.name:set(instance.Text)
end
},
Rex("TextBox") {
PlaceholderText = "Email",
Text = formData.email,
onTextChanged = function(instance)
formData.email:set(instance.Text)
end
},
Rex("TextBox") {
PlaceholderText = "Message",
Text = formData.message,
onTextChanged = function(instance)
formData.message:set(instance.Text)
end
},
Rex("TextButton") {
Text = "Submit",
BackgroundColor3 = isValid:map(function(valid)
return valid and Color3.fromRGB(0, 255, 0) or Color3.fromRGB(128, 128, 128)
end),
onClick = isValid:map(function(valid)
return valid and function()
print("Submitting form...")
-- Submit logic here
end or nil
end)
}
}
}
end
Debugging State
Rex provides helpful debugging tools:
local function DebugExample()
local count = Rex.useState(0)
-- Watch state changes
Rex.useEffect(function()
print("Count changed to:", count:get())
end, {count})
return Rex("TextButton") {
Text = "Count: " .. tostring(count:get()),
onClick = function()
count:update(function(current) return current + 1 end)
end
}
end
What’s Next?
Now that you understand state management, you’re ready to:
- Try the Quick Start Guide for hands-on practice
- Learn about Component Lifecycle
- Explore Advanced State Patterns
- Understand Context for sharing state across components
Common Questions
Q: When should I use useState
vs useDeepState
?
A: Use useState
for simple values (numbers, strings, booleans). Use useDeepState
when you need to track changes to nested object properties.
Q: Can I use multiple state objects in one component?
A: Absolutely! Components can have as many state objects as needed. Group related state together for better organization.
Q: How do I share state between components?
A: Use Context for sharing state across multiple components, or pass state as props for parent-child communication.
Q: Why isn’t my computed value updating?
A: Make sure you’ve included all dependencies in the dependency array for useComputed
, or use useAutoComputed
for automatic dependency tracking.