Event Handling

Learn how to handle user interactions in Rex with unified camelCase event system for buttons, text inputs, and other UI elements.

Last updated: 6/23/2025
Version: 0.2.0

Rex provides a unified, intuitive event handling system that uses camelCase naming conventions familiar to web developers. Instead of remembering Roblox’s specific event names, you can use consistent, predictable event handlers.

Basic Event Handling

Events are passed as props to Rex elements using camelCase naming:

Rex("TextButton") {
    Text = "Click me!",
    Size = UDim2.fromOffset(100, 40),
    onClick = function()
        print("Button was clicked!")
    end
}

Common Event Types

Click Events

Rex("TextButton") {
    Text = "Interactive Button",
    
    -- Left click (most common)
    onClick = function()
        print("Left clicked!")
    end,
    
    -- Right click
    onRightClick = function()
        print("Right clicked!")
    end,
    
    -- Generic activation (Enter key, etc.)
    onActivated = function()
        print("Button activated!")
    end
}

Hover Events

local function HoverButton()
    local isHovered = Rex.useState(false)
    
    return Rex("TextButton") {
        Text = "Hover me!",
        BackgroundColor3 = isHovered:map(function(hovered)
            return hovered and Color3.fromRGB(100, 150, 255) or Color3.fromRGB(70, 130, 255)
        end),
        
        onHover = function()
            isHovered:set(true)
            print("Mouse entered!")
        end,
        
        onLeave = function()
            isHovered:set(false)
            print("Mouse left!")
        end
    }
end

Focus Events

Rex("TextBox") {
    PlaceholderText = "Enter your name...",
    
    onFocus = function()
        print("TextBox gained focus")
    end,
    
    onBlur = function(instance, enterPressed, inputObject)
        print("TextBox lost focus")
        print("Enter was pressed:", enterPressed)
    end,
    
    -- Alternative name for onBlur
    onFocusLost = function(instance, enterPressed, inputObject)
        print("Same as onBlur")
    end
}

Text Input Events

local function SearchBox()
    local searchText = Rex.useState("")
    
    return Rex("TextBox") {
        PlaceholderText = "Search...",
        Text = searchText,
        
        onTextChanged = function(instance)
            searchText:set(instance.Text)
            print("Search text:", instance.Text)
        end,
        
        onFocusLost = function(instance, enterPressed)
            if enterPressed then
                print("Search submitted:", searchText:get())
            end
        end
    end
end

Event Handler Patterns

State Updates in Events

local function Counter()
    local count = Rex.useState(0)
    
    return Rex("Frame") {
        children = {
            Rex("TextLabel") {
                Text = count:map(function(c) return `Count: {c}` end)
            },
            Rex("TextButton") {
                Text = "+",
                onClick = function()
                    count:update(function(current) return current + 1 end)
                end
            },
            Rex("TextButton") {
                Text = "-",
                onClick = function()
                    count:update(function(current) return math.max(0, current - 1) end)
                end
            },
            Rex("TextButton") {
                Text = "Reset",
                onClick = function()
                    count:set(0)
                end
            }
        }
    end
end

Conditional Event Handlers

local function ConditionalButton()
    local isEnabled = Rex.useState(true)
    local clickCount = Rex.useState(0)
    
    return Rex("TextButton") {
        Text = isEnabled:map(function(enabled) 
            return enabled and "Click me!" or "Disabled"
        end),
        
        BackgroundColor3 = isEnabled:map(function(enabled)
            return enabled and Color3.fromRGB(70, 130, 255) or Color3.fromRGB(100, 100, 100)
        end),
        
        -- Conditional event handler
        onClick = isEnabled:map(function(enabled)
            return enabled and function()
                clickCount:update(function(count) return count + 1 end)
                
                -- Disable after 5 clicks
                if clickCount:get() >= 5 then
                    isEnabled:set(false)
                end
            end or nil
        end)
    end
end

Event Handler with Cleanup

local function TimerButton()
    local isRunning = Rex.useState(false)
    local timeLeft = Rex.useState(10)
    
    Rex.useEffect(function()
        if not isRunning:get() then
            return
        end
        
        local connection = game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
            timeLeft:update(function(current)
                local newTime = current - deltaTime
                if newTime <= 0 then
                    isRunning:set(false)
                    return 10 -- Reset timer
                end
                return newTime
            end)
        end)
        
        -- Cleanup function
        return function()
            connection:Disconnect()
        end
    end, {isRunning})
    
    return Rex("TextButton") {
        Text = isRunning:map(function(running)
            return running and `Time: {math.ceil(timeLeft:get())}` or "Start Timer"
        end),
        
        onClick = function()
            isRunning:update(function(current) return not current end)
        end
    }
end

Event Mapping Reference

Rex automatically maps camelCase event names to Roblox events:

Core Events

Rex EventRoblox EventDescription
onClickMouseButton1ClickLeft mouse button click
onRightClickMouseButton2ClickRight mouse button click
onHoverMouseEnterMouse enters the element
onLeaveMouseLeaveMouse leaves the element
onFocusFocusedElement gains focus
onBlurFocusLostElement loses focus
onFocusLostFocusLostAlternative to onBlur
onTextChangedChangedText content changes (TextBox)
onActivatedActivatedButton activation (Enter key, etc.)

Mouse Events

Rex EventRoblox EventDescription
onMouseEnterMouseEnterAlternative to onHover
onMouseLeaveMouseLeaveAlternative to onLeave
onMouseMovedMouseMovedMouse moves over element
onMouseWheelForwardMouseWheelForwardMouse wheel scrolled forward
onMouseWheelBackwardMouseWheelBackwardMouse wheel scrolled backward
onMouseDownMouseButton1DownLeft mouse button pressed down
onMouseUpMouseButton1UpLeft mouse button released
onRightMouseDownMouseButton2DownRight mouse button pressed down
onRightMouseUpMouseButton2UpRight mouse button released
onDoubleClickMouseButton1DoubleClickDouble-click detected

Touch Events

Rex EventRoblox EventDescription
onTouchTapTouchTapTouch tap gesture
onTouchLongPressTouchLongPressTouch long press gesture
onTouchPanTouchPanTouch pan/drag gesture
onTouchPinchTouchPinchTouch pinch gesture
onTouchRotateTouchRotateTouch rotate gesture

Input Events

Rex EventRoblox EventDescription
onInputBeganInputBeganInput begins (keyboard, gamepad, etc.)
onInputChangedInputChangedInput changes
onInputEndedInputEndedInput ends

Selection Events

Rex EventRoblox EventDescription
onSelectionGainedSelectionGainedElement gains selection focus
onSelectionLostSelectionLostElement loses selection focus

GuiObject Events

Rex EventRoblox EventDescription
onChangedChangedProperty changes on the element
onAncestryChangedAncestryChangedElement’s ancestry changes
onChildAddedChildAddedChild added to element
onChildRemovedChildRemovedChild removed from element
onDescendantAddedDescendantAddedDescendant added to element
onDescendantRemovingDescendantRemovingDescendant being removed

Specialized Events

Rex EventRoblox EventDescription
onCanvasPositionChangedCanvasPositionChangedScrollingFrame canvas position changes
onPropertyChangedPropertyChangedSignalGeneric property change signal

Extended Event Examples

Mouse Events

local function MouseTracker()
    local mousePosition = Rex.useState(Vector2.new(0, 0))
    local isPressed = Rex.useState(false)
    
    return Rex("Frame") {
        Size = UDim2.fromScale(1, 1),
        BackgroundColor3 = Color3.fromRGB(50, 50, 50),
        
        onMouseMoved = function(x, y)
            mousePosition:set(Vector2.new(x, y))
        end,
        
        onMouseDown = function()
            isPressed:set(true)
        end,
        
        onMouseUp = function()
            isPressed:set(false)
        end,
        
        onMouseWheelForward = function()
            print("Scrolled up")
        end,
        
        onMouseWheelBackward = function()
            print("Scrolled down")
        end,
        
        children = {
            Rex("TextLabel") {
                Text = mousePosition:map(function(pos)
                    return `Mouse: {math.floor(pos.X)}, {math.floor(pos.Y)}`
                end),
                TextColor3 = isPressed:map(function(pressed)
                    return pressed and Color3.fromRGB(255, 100, 100) or Color3.fromRGB(255, 255, 255)
                end)
            }
        }
    }
end

Touch Events

local function TouchGestureDemo()
    local gestureInfo = Rex.useState("No gesture")
    
    return Rex("Frame") {
        Size = UDim2.fromScale(0.8, 0.8),
        Position = UDim2.fromScale(0.1, 0.1),
        BackgroundColor3 = Color3.fromRGB(70, 130, 255),
        
        onTouchTap = function()
            gestureInfo:set("Tapped!")
        end,
        
        onTouchLongPress = function()
            gestureInfo:set("Long pressed!")
        end,
        
        onTouchPan = function(positions, totalTranslation, velocity, state)
            gestureInfo:set(`Panning: {state}`)
        end,
        
        onTouchPinch = function(touchPositions, scale, velocity, state)
            gestureInfo:set(`Pinching: Scale {scale}`)
        end,
        
        onTouchRotate = function(touchPositions, rotation, velocity, state)
            gestureInfo:set(`Rotating: {math.deg(rotation)}°`)
        end,
        
        children = {
            Rex("TextLabel") {
                Text = gestureInfo,
                Size = UDim2.fromScale(1, 1),
                TextScaled = true,
                BackgroundTransparency = 1,
                TextColor3 = Color3.fromRGB(255, 255, 255)
            }
        }
    end
end

Input Events

local function KeyboardListener()
    local currentKey = Rex.useState("None")
    local keyState = Rex.useState("Released")
    
    return Rex("Frame") {
        Size = UDim2.fromScale(1, 1),
        
        onInputBegan = function(input, gameProcessed)
            if not gameProcessed and input.UserInputType == Enum.UserInputType.Keyboard then
                currentKey:set(input.KeyCode.Name)
                keyState:set("Pressed")
            end
        end,
        
        onInputEnded = function(input, gameProcessed)
            if not gameProcessed and input.UserInputType == Enum.UserInputType.Keyboard then
                keyState:set("Released")
            end
        end,
        
        children = {
            Rex("TextLabel") {
                Text = Rex.useComputed(function()
                    return `Last Key: {currentKey:get()} ({keyState:get()})`
                end, {currentKey, keyState}),
                Size = UDim2.fromScale(1, 0.1),
                BackgroundTransparency = 1,
                TextScaled = true
            }
        }
    }
end

ScrollingFrame Events

local function ScrollableContent()
    local scrollPosition = Rex.useState(Vector2.new(0, 0))
    
    return Rex("ScrollingFrame") {
        Size = UDim2.fromScale(0.8, 0.6),
        Position = UDim2.fromScale(0.1, 0.2),
        CanvasSize = UDim2.fromOffset(0, 1000),
        ScrollBarThickness = 8,
        
        onCanvasPositionChanged = function()
            -- Note: This event doesn't pass the canvas position directly
            -- You'd need to access it via the instance if needed
        end,
        
        children = {
            Rex("UIListLayout") {
                SortOrder = Enum.SortOrder.LayoutOrder
            },
            -- Generate content
            (function()
                local content = {}
                for i = 1, 20 do
                    table.insert(content, Rex("TextLabel") {
                        Text = `Item {i}`,
                        Size = UDim2.new(1, 0, 0, 40),
                        BackgroundColor3 = i % 2 == 0 and Color3.fromRGB(240, 240, 240) or Color3.fromRGB(255, 255, 255),
                        LayoutOrder = i
                    })
                end
                return content
            end)()
        }
    }
end

GuiObject Lifecycle Events

local function LifecycleTracker()
    local events = Rex.useState({})
    
    local function addEvent(eventType)
        events:update(function(current)
            local new = table.clone(current)
            table.insert(new, `{eventType} at {os.clock()}`)
            -- Keep only last 10 events
            if #new > 10 then
                table.remove(new, 1)
            end
            return new
        end)
    end
    
    return Rex("Frame") {
        Size = UDim2.fromScale(0.8, 0.8),
        Position = UDim2.fromScale(0.1, 0.1),
        
        onAncestryChanged = function()
            addEvent("Ancestry Changed")
        end,
        
        onChildAdded = function(child)
            addEvent(`Child Added: {child.Name}`)
        end,
        
        onChildRemoved = function(child)
            addEvent(`Child Removed: {child.Name}`)
        end,
        
        onDescendantAdded = function(descendant)
            addEvent(`Descendant Added: {descendant.Name}`)
        end,
        
        onDescendantRemoving = function(descendant)
            addEvent(`Descendant Removing: {descendant.Name}`)
        end,
        
        children = {
            Rex("ScrollingFrame") {
                Size = UDim2.fromScale(1, 1),
                CanvasSize = UDim2.fromOffset(0, 0),
                AutomaticCanvasSize = Enum.AutomaticSize.Y,
                children = {
                    Rex("UIListLayout") {
                        SortOrder = Enum.SortOrder.LayoutOrder
                    },
                    events:map(function(eventList)
                        local children = {}
                        for i, event in ipairs(eventList) do
                            table.insert(children, Rex("TextLabel") {
                                Text = event,
                                Size = UDim2.new(1, 0, 0, 25),
                                BackgroundTransparency = 1,
                                TextXAlignment = Enum.TextXAlignment.Left,
                                LayoutOrder = i,
                                key = tostring(i)
                            })
                        end
                        return children
                    end)
                }
            }
        }
    }
end

Best Practices

1. Use Descriptive Handler Names

-- Good: Descriptive function names
local function handleAddItem() ... end
local function handleRemoveItem(index) ... end
local function handleToggleVisibility() ... end

-- Less ideal: Generic names
local function onClick() ... end
local function handleEvent() ... end

2. Avoid Inline Functions for Complex Logic

-- Good: Extract complex logic
local function ComplexComponent()
    local function handleComplexClick()
        -- Complex logic here
        doSomethingComplex()
        updateMultipleStates()
        performValidation()
    end
    
    return Rex("TextButton") {
        onClick = handleComplexClick
    }
end

-- Less ideal: Inline complex logic
local function ComplexComponent()
    return Rex("TextButton") {
        onClick = function()
            -- Lots of complex logic inline
        end
    }
end

3. Use State Updates for Predictable Behavior

-- Good: State-driven updates
local function ToggleButton()
    local isToggled = Rex.useState(false)
    
    return Rex("TextButton") {
        Text = isToggled:map(function(toggled) return toggled and "ON" or "OFF" end),
        onClick = function()
            isToggled:update(function(current) return not current end)
        end
    }
end

4. Handle Edge Cases

local function SafeButton()
    local isProcessing = Rex.useState(false)
    
    return Rex("TextButton") {
        Text = isProcessing:map(function(processing) 
            return processing and "Processing..." or "Click me"
        end),
        
        onClick = isProcessing:map(function(processing)
            -- Prevent double-clicks during processing
            return processing and nil or function()
                isProcessing:set(true)
                
                -- Simulate async operation
                task.spawn(function()
                    wait(2)
                    isProcessing:set(false)
                end)
            end
        end)
    }
end