Reactivity System
Understand Rex's reactivity system - how state changes automatically trigger UI updates and the principles behind reactive programming.
Reactivity is the core principle that makes Rex powerful and intuitive. When data changes, the UI automatically updates to reflect those changes without manual intervention. This creates a direct connection between your application’s state and its visual representation.
What is Reactivity?
In traditional imperative UI programming, you manually update UI elements when data changes:
-- Traditional imperative approach
local count = 0
local label = Instance.new("TextLabel")
label.Text = "Count: " .. count
-- Manual update required
count = count + 1
label.Text = "Count: " .. count -- Must remember to update UI
With Rex’s reactive approach, the UI automatically updates when state changes:
-- Rex reactive approach
local count = Rex.useState(0)
local label = Rex("TextLabel") {
Text = count:map(function(c) return `Count: {c}` end)
}
-- Automatic update
count:set(count:get() + 1) -- UI updates automatically
The Reactive Flow
Rex’s reactivity follows a clear flow:
- State Creation: Create reactive state with
useState
,useComputed
, etc. - UI Binding: Bind state to UI properties or use in computed values
- Change Detection: When state changes, Rex detects the change
- Update Propagation: Changes propagate to all dependent UI elements
- Efficient Rendering: Only affected elements are updated
local function ReactiveFlow()
-- 1. State Creation
local name = Rex.useState("Player")
local level = Rex.useState(1)
-- 2. Computed state (automatically updates when dependencies change)
local displayText = Rex.useComputed(function()
return `{name:get()} (Level {level:get()})`
end, {name, level})
-- 3. UI Binding
return Rex("TextLabel") {
Text = displayText -- Automatically updates when name or level changes
}
end
Types of Reactivity
1. Direct State Binding
Bind state directly to properties:
local color = Rex.useState(Color3.fromRGB(255, 0, 0))
Rex("Frame") {
BackgroundColor3 = color -- Direct binding
}
2. Transformed Binding
Transform state values using map
:
local health = Rex.useState(100)
Rex("TextLabel") {
Text = health:map(function(h) return `Health: {h}%` end)
}
3. Computed Reactivity
Derive values from multiple states:
local width = Rex.useState(100)
local height = Rex.useState(200)
local area = Rex.useComputed(function()
return width:get() * height:get()
end, {width, height})
Rex("TextLabel") {
Text = area:map(function(a) return `Area: {a}` end)
}
4. Effect-Based Reactivity
Run side effects when state changes:
local playerCount = Rex.useState(0)
Rex.useEffect(function()
local count = playerCount:get()
print(`Player count changed to: {count}`)
-- Update game title
game.Name = `My Game ({count} players)`
end, {playerCount})
Reactive Children
Rex supports reactive children that automatically update when state changes:
local function DynamicList()
local items = Rex.useState({"Apple", "Banana", "Cherry"})
return Rex("ScrollingFrame") {
children = {
Rex("UIListLayout") {},
-- Reactive children - automatically updates when items change
items:map(function(itemList)
local children = {}
for i, item in ipairs(itemList) do
table.insert(children, Rex("TextLabel") {
Text = item,
key = item, -- Key for efficient diffing
Size = UDim2.new(1, 0, 0, 30)
})
end
return children
end)
}
}
end
Dependency Tracking
Rex automatically tracks dependencies in computed states and effects:
Manual Dependency Declaration
local a = Rex.useState(1)
local b = Rex.useState(2)
-- Explicitly declare dependencies
local sum = Rex.useComputed(function()
return a:get() + b:get()
end, {a, b}) -- Manual dependency list
Automatic Dependency Tracking
local a = Rex.useState(1)
local b = Rex.useState(2)
-- Dependencies automatically detected
local sum = Rex.useAutoComputed(function()
return a:get() + b:get() -- Rex automatically tracks a and b
end)
Reactive Patterns
1. Conditional Reactivity
local function ConditionalUI()
local isLoggedIn = Rex.useState(false)
local userName = Rex.useState("")
return Rex("Frame") {
children = isLoggedIn:map(function(loggedIn)
if loggedIn then
return {
Rex("TextLabel") {
Text = userName:map(function(name) return `Welcome, {name}!` end)
},
Rex("TextButton") {
Text = "Logout",
onClick = function() isLoggedIn:set(false) end
}
}
else
return {
Rex("TextLabel") { Text = "Please log in" },
Rex("TextButton") {
Text = "Login",
onClick = function()
userName:set("Player")
isLoggedIn:set(true)
end
}
}
end
end)
}
end
2. Cascading Updates
local function CascadingExample()
local baseValue = Rex.useState(10)
-- Each computed state depends on the previous one
local doubled = Rex.useComputed(function()
return baseValue:get() * 2
end, {baseValue})
local formatted = Rex.useComputed(function()
return `Value: {doubled:get()}`
end, {doubled})
local styled = Rex.useComputed(function()
local value = doubled:get()
return {
text = formatted:get(),
color = value > 50 and Color3.fromRGB(0, 255, 0) or Color3.fromRGB(255, 0, 0)
}
end, {doubled, formatted})
return Rex("TextLabel") {
Text = styled:map(function(s) return s.text end),
TextColor3 = styled:map(function(s) return s.color end)
}
end
3. Cross-Component Reactivity with Context
-- Global theme context
local ThemeContext = Rex.createContext({
primary = Color3.fromRGB(70, 130, 255),
background = Color3.fromRGB(30, 30, 40)
})
local function App()
local currentTheme = Rex.useState({
primary = Color3.fromRGB(70, 130, 255),
background = Color3.fromRGB(30, 30, 40)
})
return Rex.Provider {
context = ThemeContext,
value = currentTheme,
children = {
ThemedComponent(),
ThemeToggleButton { theme = currentTheme }
}
}
end
local function ThemedComponent()
local theme = Rex.useContext(ThemeContext)
return Rex("Frame") {
BackgroundColor3 = theme:map(function(t) return t.background end),
children = {
Rex("TextLabel") {
Text = "Themed Component",
TextColor3 = theme:map(function(t) return t.primary end)
}
}
}
end
Performance and Reactivity
Batching Updates
Rex automatically batches updates for better performance:
local function BatchedUpdates()
local x = Rex.useState(0)
local y = Rex.useState(0)
local z = Rex.useState(0)
-- Multiple updates in one action
local function updateAll()
Rex.batch(function()
x:set(10)
y:set(20)
z:set(30)
end) -- Single UI update instead of three
end
return Rex("TextButton") {
Text = "Update All",
onClick = updateAll
}
end
Memoization
Use memoization for expensive computations:
local function ExpensiveComputation()
local data = Rex.useState(largeDataSet)
-- Memoized expensive calculation
local processedData = Rex.useComputed(function()
return performExpensiveProcessing(data:get())
end, {data}, "expensiveProcess") -- Memoization key
return Rex("TextLabel") {
Text = processedData:map(function(result) return `Processed: {#result} items` end)
}
end
Debugging Reactivity
1. Use Effects to Debug State Changes
local count = Rex.useState(0)
-- Debug effect
Rex.useEffect(function()
print(`Count changed to: {count:get()}`)
print(`Call stack:`, debug.traceback())
end, {count})
2. Name Your States
-- Add meaningful names for debugging
local playerHealth = Rex.useState(100, "playerHealth")
local playerMana = Rex.useState(50, "playerMana")
3. Monitor Dependency Chains
local function DebugDependencies()
local a = Rex.useState(1)
local b = Rex.useComputed(function() return a:get() * 2 end, {a})
local c = Rex.useComputed(function() return b:get() + 10 end, {b})
-- Debug each level
Rex.useEffect(function() print("a changed:", a:get()) end, {a})
Rex.useEffect(function() print("b changed:", b:get()) end, {b})
Rex.useEffect(function() print("c changed:", c:get()) end, {c})
end
Best Practices for Reactivity
1. Keep State Close to Where It’s Used
-- Good: Local state
local function Counter()
local count = Rex.useState(0) -- Used only in this component
return Rex("TextLabel") {
Text = count:map(function(c) return `Count: {c}` end)
}
end
-- Less ideal: Global state for local concerns
local globalCount = Rex.useState(0) -- Used everywhere, harder to track
2. Use Computed States for Derived Data
-- Good: Computed state for derived data
local items = Rex.useState({...})
local itemCount = Rex.useComputed(function()
return #items:get()
end, {items})
-- Less ideal: Manual synchronization
local items = Rex.useState({...})
local itemCount = Rex.useState(0) -- Must manually keep in sync
3. Minimize Deep State
-- Good: Flat state structure
local playerName = Rex.useState("Player")
local playerLevel = Rex.useState(1)
local playerHealth = Rex.useState(100)
-- Less ideal: Deep nested state
local player = Rex.useDeepState({
personal = {
profile = { name = "Player" },
stats = { level = 1, health = 100 }
}
})
Reactivity is what makes Rex feel magical - your UI stays perfectly in sync with your data automatically. Understanding these patterns will help you build more efficient and maintainable applications.