Components
Learn how to build reusable, composable components in Rex and patterns for component architecture and data flow.
Components are the building blocks of Rex applications. They are functions that return Rex elements and can accept props for customization. Rex promotes composition over inheritance, allowing you to build complex UIs from simple, reusable components.
Basic Components
A component is simply a function that returns a Rex element:
local function Button(props)
return Rex("TextButton") {
Text = props.text or "Button",
Size = props.size or UDim2.fromOffset(100, 40),
onClick = props.onClick
}
end
-- Usage
local myButton = Button {
text = "Click me!",
size = UDim2.fromOffset(150, 50),
onClick = function() print("Clicked!") end
}
Component Props
Props are the way to pass data to components. They make components reusable and customizable:
local function PlayerCard(props)
return Rex("Frame") {
Size = UDim2.fromOffset(200, 100),
BackgroundColor3 = Color3.fromRGB(50, 50, 60),
children = {
Rex("UICorner") { CornerRadius = UDim.new(0, 8) },
Rex("UIPadding") {
PaddingTop = UDim.new(0, 10),
PaddingLeft = UDim.new(0, 10),
PaddingRight = UDim.new(0, 10),
PaddingBottom = UDim.new(0, 10)
},
Rex("TextLabel") {
Text = props.playerName or "Unknown Player",
Size = UDim2.new(1, 0, 0.5, 0),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextScaled = true,
Font = Enum.Font.SourceSansBold
},
Rex("TextLabel") {
Text = `Level {props.level or 1}`,
Size = UDim2.new(1, 0, 0.5, 0),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundTransparency = 1,
TextColor3 = Color3.fromRGB(150, 150, 150),
TextScaled = true
}
}
}
end
-- Usage
local card = PlayerCard {
playerName = "Alex",
level = 25
}
Stateful Components
Components can have their own internal state:
local function Counter(props)
local count = Rex.useState(props.initialValue or 0)
local isHovered = Rex.useState(false)
return Rex("Frame") {
Size = UDim2.fromOffset(200, 100),
BackgroundColor3 = Color3.fromRGB(30, 30, 40),
children = {
Rex("TextLabel") {
Text = count:map(function(c) return `Count: {c}` end),
Size = UDim2.new(1, 0, 0.5, 0),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextScaled = true
},
Rex("TextButton") {
Text = "Increment",
Size = UDim2.new(1, 0, 0.5, 0),
Position = UDim2.new(0, 0, 0.5, 0),
BackgroundColor3 = isHovered:map(function(hovered)
return hovered and Color3.fromRGB(90, 150, 255) or Color3.fromRGB(70, 130, 255)
end),
onClick = function()
count:update(function(current) return current + 1 end)
-- Call parent callback if provided
if props.onCountChange then
props.onCountChange(count:get())
end
end,
onHover = function() isHovered:set(true) end,
onLeave = function() isHovered:set(false) end
}
}
}
end
Component Composition
Build complex components by composing simpler ones:
-- Base components
local function IconButton(props)
return Rex("TextButton") {
Text = props.icon or "?",
Size = props.size or UDim2.fromOffset(30, 30),
BackgroundColor3 = props.color or Color3.fromRGB(70, 130, 255),
TextColor3 = Color3.new(1, 1, 1),
onClick = props.onClick
}
end
local function Badge(props)
return Rex("Frame") {
Size = UDim2.fromOffset(20, 20),
Position = UDim2.new(1, -10, 0, -10),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundColor3 = Color3.fromRGB(255, 100, 100),
children = {
Rex("UICorner") { CornerRadius = UDim.new(0.5, 0) },
Rex("TextLabel") {
Text = tostring(props.count or 0),
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextScaled = true,
Font = Enum.Font.SourceSansBold
}
}
}
end
-- Composed component
local function NotificationButton(props)
local notificationCount = props.notificationCount or 0
return Rex("Frame") {
Size = UDim2.fromOffset(30, 30),
BackgroundTransparency = 1,
children = {
IconButton {
icon = "🔔",
onClick = props.onClick,
color = Color3.fromRGB(255, 150, 0)
},
-- Conditionally render badge
notificationCount > 0 and Badge { count = notificationCount } or nil
}
}
end
Higher-Order Components
Create components that enhance other components:
local function withLoading(WrappedComponent)
return function(props)
local isLoading = props.isLoading or false
if isLoading then
return Rex("Frame") {
Size = props.size or UDim2.fromScale(1, 1),
BackgroundColor3 = Color3.fromRGB(50, 50, 60),
children = {
Rex("TextLabel") {
Text = "Loading...",
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
TextScaled = true
}
}
}
end
return WrappedComponent(props)
end
end
-- Usage
local LoadablePlayerCard = withLoading(PlayerCard)
local card = LoadablePlayerCard {
playerName = "Alex",
level = 25,
isLoading = false
}
Children Props Pattern
Components can accept and render children:
local function Card(props)
return Rex("Frame") {
Size = props.size or UDim2.fromOffset(300, 200),
BackgroundColor3 = Color3.fromRGB(40, 40, 50),
children = {
Rex("UICorner") { CornerRadius = UDim.new(0, 12) },
Rex("UIPadding") {
PaddingTop = UDim.new(0, 15),
PaddingLeft = UDim.new(0, 15),
PaddingRight = UDim.new(0, 15),
PaddingBottom = UDim.new(0, 15)
},
Rex("UIListLayout") {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 10)
},
-- Render children passed from parent
props.children
}
}
end
-- Usage with children
local myCard = Card {
size = UDim2.fromOffset(400, 300),
children = {
Rex("TextLabel") {
Text = "Card Title",
LayoutOrder = 1,
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = 1,
TextColor3 = Color3.new(1, 1, 1),
Font = Enum.Font.SourceSansBold
},
Rex("TextLabel") {
Text = "Card content goes here...",
LayoutOrder = 2,
Size = UDim2.new(1, 0, 1, -40),
BackgroundTransparency = 1,
TextColor3 = Color3.fromRGB(200, 200, 200),
TextWrapped = true
}
}
}
Render Props Pattern
Pass functions as props to share logic:
local function DataProvider(props)
local data = Rex.useState(nil)
local loading = Rex.useState(true)
local error = Rex.useState(nil)
-- Simulate data fetching
Rex.useEffect(function()
loading:set(true)
error:set(nil)
task.spawn(function()
wait(1) -- Simulate network delay
local success, result = pcall(props.fetchData)
if success then
data:set(result)
else
error:set("Failed to fetch data")
end
loading:set(false)
end)
end, {})
-- Call render prop with current state
return props.render({
data = data:get(),
loading = loading:get(),
error = error:get()
})
end
-- Usage
local function App()
return DataProvider {
fetchData = function()
return {
name = "Player",
level = 10,
coins = 1500
}
end,
render = function(state)
if state.loading then
return Rex("TextLabel") { Text = "Loading..." }
end
if state.error then
return Rex("TextLabel") {
Text = `Error: {state.error}`,
TextColor3 = Color3.fromRGB(255, 100, 100)
}
end
return PlayerCard {
playerName = state.data.name,
level = state.data.level
}
end
}
end
Custom Hooks Pattern
Extract stateful logic into reusable functions:
-- Custom hook for toggle functionality
local function useToggle(initialValue)
local value = Rex.useState(initialValue or false)
local toggle = function()
value:update(function(current) return not current end)
end
local setTrue = function() value:set(true) end
local setFalse = function() value:set(false) end
return value, {
toggle = toggle,
setTrue = setTrue,
setFalse = setFalse
}
end
-- Custom hook for API data
local function useApiData(endpoint, dependencies)
local data = Rex.useState(nil)
local loading = Rex.useState(false)
local error = Rex.useState(nil)
local fetchData = function()
loading:set(true)
error:set(nil)
task.spawn(function()
local success, result = pcall(function()
-- Simulate API call
wait(0.5)
return fetchFromApi(endpoint)
end)
if success then
data:set(result)
else
error:set("Failed to fetch data")
end
loading:set(false)
end)
end
Rex.useEffect(fetchData, dependencies or {})
return {
data = data,
loading = loading,
error = error,
refetch = fetchData
}
end
-- Using custom hooks
local function ToggleButton()
local isToggled, toggle = useToggle(false)
return Rex("TextButton") {
Text = isToggled:map(function(toggled) return toggled and "ON" or "OFF" end),
onClick = toggle.toggle
}
end
local function PlayerProfile(props)
local playerData = useApiData(`/players/{props.playerId}`, {props.playerId})
if playerData.loading:get() then
return Rex("TextLabel") { Text = "Loading player..." }
end
if playerData.error:get() then
return Rex("TextLabel") {
Text = `Error: {playerData.error:get()}`,
TextColor3 = Color3.fromRGB(255, 100, 100)
}
end
return PlayerCard {
playerName = playerData.data:get().name,
level = playerData.data:get().level
}
end
Component Communication Patterns
Parent-Child Communication
local function ParentComponent()
local childData = Rex.useState("Hello from parent")
local childResponse = Rex.useState("")
return Rex("Frame") {
children = {
Rex("TextLabel") {
Text = `Child said: {childResponse:get()}`
},
ChildComponent {
data = childData:get(),
onResponse = function(response)
childResponse:set(response)
end
}
}
}
end
local function ChildComponent(props)
return Rex("TextButton") {
Text = `Received: {props.data}`,
onClick = function()
props.onResponse("Hello from child!")
end
}
end
Sibling Communication via Context
local AppStateContext = Rex.createContext({
user = nil,
notifications = {}
})
local function App()
local appState = Rex.useState({
user = { name = "Player", level = 1 },
notifications = {}
})
return Rex.Provider {
context = AppStateContext,
value = appState,
children = {
Header(),
MainContent(),
NotificationPanel()
}
}
end
local function Header()
local appState = Rex.useContext(AppStateContext)
return Rex("Frame") {
children = {
Rex("TextLabel") {
Text = appState:map(function(state)
return `Welcome, {state.user.name}!`
end)
}
}
}
end
local function NotificationPanel()
local appState = Rex.useContext(AppStateContext)
return Rex("Frame") {
children = {
Rex("TextLabel") {
Text = appState:map(function(state)
return `{#state.notifications} notifications`
end)
}
}
}
end
Component Best Practices
1. Single Responsibility Principle
-- Good: Component has single responsibility
local function UserAvatar(props)
return Rex("ImageLabel") {
Image = props.avatarUrl,
Size = props.size or UDim2.fromOffset(50, 50)
}
end
local function UserInfo(props)
return Rex("Frame") {
children = {
UserAvatar { avatarUrl = props.user.avatar, size = props.avatarSize },
Rex("TextLabel") { Text = props.user.name }
}
}
end
-- Less ideal: Component doing too much
local function UserEverything(props)
-- Handles avatar, info, settings, notifications, etc.
end
2. Prop Validation
local function Button(props)
-- Validate required props
assert(props.text, "Button requires text prop")
assert(typeof(props.onClick) == "function", "Button requires onClick function")
return Rex("TextButton") {
Text = props.text,
onClick = props.onClick
}
end
3. Default Props
local function Card(props)
-- Set defaults
local size = props.size or UDim2.fromOffset(300, 200)
local backgroundColor = props.backgroundColor or Color3.fromRGB(50, 50, 60)
local cornerRadius = props.cornerRadius or UDim.new(0, 8)
return Rex("Frame") {
Size = size,
BackgroundColor3 = backgroundColor,
children = {
Rex("UICorner") { CornerRadius = cornerRadius },
props.children
}
}
end
4. Composable Design
-- Good: Small, composable components
local function Icon(props) ... end
local function Text(props) ... end
local function Button(props) ... end
local function IconButton(props)
return Button {
children = {
Icon { name = props.icon },
Text { content = props.text }
},
onClick = props.onClick
}
end
-- Less ideal: Monolithic components
local function MegaComponent(props)
-- Tries to handle everything internally
end
Components are the heart of Rex applications. By following these patterns and principles, you can build maintainable, reusable, and composable user interfaces that scale with your application’s complexity.