List Rendering & Children

Learn how to efficiently render dynamic lists and manage children in Rex using the :each() method and keys.

Last updated: 6/29/2025
Version: 0.2.0

Dynamic list rendering is one of the most common patterns in UI development. Rex provides powerful tools for rendering lists efficiently with the :each() method, intelligent reconciliation, and proper key management.

The :each() Method

Rex’s :each() method provides a clean, reactive way to render lists from array state:

local items = Rex.useState({"Apple", "Banana", "Cherry"})

Rex("ScrollingFrame") {
    children = {
        Rex("UIListLayout") {},
        items:each(function(item, index)
            return Rex("TextLabel") {
                Text = `{index}: {item}`,
                key = item -- Important for efficient updates
            }
        end)
    }
}

Automatic Reactivity

When the array state changes, the UI automatically updates:

local function TodoApp()
    local todos = Rex.useState({
        "Learn Rex",
        "Build amazing UI",
        "Ship the project"
    })
    
    return Rex("Frame") {
        children = {
            Rex("UIListLayout") {},
            
            -- Header
            Rex("TextLabel") {
                Text = todos:map(function(list)
                    return `Tasks: {#list}`
                end),
                LayoutOrder = 1
            },
            
            -- Dynamic todo list
            todos:each(function(todo, index)
                return Rex("TextLabel") {
                    Text = `{index}. {todo}`,
                    key = todo,
                    LayoutOrder = index + 1
                }
            end),
            
            -- Add button
            Rex("TextButton") {
                Text = "Add Random Task",
                LayoutOrder = 1000,
                onClick = function()
                    local tasks = {"Review code", "Write tests", "Update docs", "Fix bugs"}
                    local randomTask = tasks[math.random(1, #tasks)]
                    todos:push(randomTask)
                end
            }
        }
    }
end

Interactive Lists

Create interactive lists with buttons, inputs, and state management:

local function ShoppingList()
    local items = Rex.useState({
        {id = 1, name = "Milk", bought = false},
        {id = 2, name = "Bread", bought = true},
        {id = 3, name = "Eggs", bought = false}
    })
    
    local function toggleItem(itemId)
        items:update(function(currentItems)
            local newItems = table.clone(currentItems)
            for i, item in ipairs(newItems) do
                if item.id == itemId then
                    newItems[i] = {
                        id = item.id,
                        name = item.name,
                        bought = not item.bought
                    }
                    break
                end
            end
            return newItems
        end)
    end
    
    local function removeItem(itemId)
        items:update(function(currentItems)
            local newItems = {}
            for _, item in ipairs(currentItems) do
                if item.id ~= itemId then
                    table.insert(newItems, item)
                end
            end
            return newItems
        end)
    end
    
    return Rex("ScrollingFrame") {
        Size = UDim2.fromScale(1, 1),
        children = {
            Rex("UIListLayout") { Padding = UDim.new(0, 5) },
            
            items:each(function(item, index)
                return Rex("Frame") {
                    Size = UDim2.new(1, 0, 0, 50),
                    BackgroundColor3 = item.bought 
                        and Color3.fromRGB(100, 255, 100)  -- Green when bought
                        or Color3.fromRGB(255, 255, 255),  -- White when pending
                    key = tostring(item.id), -- Use stable ID
                    
                    children = {
                        Rex("UIListLayout") {
                            FillDirection = Enum.FillDirection.Horizontal,
                            VerticalAlignment = Enum.VerticalAlignment.Center,
                            Padding = UDim.new(0, 10)
                        },
                        
                        -- Item name
                        Rex("TextLabel") {
                            Text = item.name,
                            Size = UDim2.new(0.6, 0, 1, 0),
                            TextStrikethrough = item.bought,
                            TextColor3 = item.bought 
                                and Color3.fromRGB(100, 100, 100)
                                or Color3.fromRGB(0, 0, 0),
                            LayoutOrder = 1
                        },
                        
                        -- Toggle button
                        Rex("TextButton") {
                            Text = item.bought and "↶ Undo" or "✓ Buy",
                            Size = UDim2.new(0.25, 0, 0.8, 0),
                            BackgroundColor3 = item.bought
                                and Color3.fromRGB(255, 200, 100)
                                or Color3.fromRGB(100, 200, 255),
                            onClick = function()
                                toggleItem(item.id)
                            end,
                            LayoutOrder = 2
                        },
                        
                        -- Remove button
                        Rex("TextButton") {
                            Text = "✕",
                            Size = UDim2.new(0.15, 0, 0.8, 0),
                            BackgroundColor3 = Color3.fromRGB(255, 100, 100),
                            TextColor3 = Color3.new(1, 1, 1),
                            onClick = function()
                                removeItem(item.id)
                            end,
                            LayoutOrder = 3
                        }
                    }
                }
            end)
        }
    }
end

Key Management

Keys are critical for efficient list updates. Rex uses keys to identify which elements have changed, been added, or removed.

Why Keys Matter

Without keys, Rex must guess which elements correspond to which data:

-- ❌ Without keys - inefficient
items:each(function(item, index)
    return ItemComponent { item = item }
    -- Rex can't track which element is which
end)

-- When items reorder: [A, B, C] → [B, A, C]
-- Rex might recreate all elements instead of just moving them

With keys, Rex knows exactly which elements to reuse:

-- ✅ With keys - efficient
items:each(function(item, index)
    return ItemComponent { 
        item = item, 
        key = item.id -- Stable identifier
    }
end)

-- When items reorder: Rex moves existing elements efficiently

Key Selection Strategies

Best: Stable Unique IDs

todos:each(function(todo, index)
    return TodoItem { 
        todo = todo, 
        key = tostring(todo.id) -- Database ID, UUID, etc.
    }
end)

Good: Content-Based Keys (for simple, unique data)

colors:each(function(color, index)
    return ColorSwatch { 
        color = color, 
        key = color -- "red", "blue", etc. if unique
    }
end)

Avoid: Index-Based Keys

-- ❌ Don't use index as key
items:each(function(item, index)
    return ItemComponent { 
        item = item, 
        key = tostring(index) -- Breaks on reordering!
    }
end)

Performance Patterns

Filtering and Searching

Combine reactive state with :each() for dynamic filtering:

local function FilteredList()
    local allItems = Rex.useState({"Apple", "Banana", "Cherry", "Date"})
    local searchText = Rex.useState("")
    
    local filteredItems = Rex.useComputed(function()
        local search = searchText:get():lower()
        if search == "" then
            return allItems:get()
        end
        
        local filtered = {}
        for _, item in ipairs(allItems:get()) do
            if item:lower():find(search) then
                table.insert(filtered, item)
            end
        end
        return filtered
    end, {allItems, searchText})
    
    return Rex("Frame") {
        children = {
            -- Search box
            Rex("TextBox") {
                PlaceholderText = "Search items...",
                Text = searchText,
                onTextChanged = function(textBox)
                    searchText:set(textBox.Text)
                end
            },
            
            -- Filtered results
            Rex("ScrollingFrame") {
                children = {
                    Rex("UIListLayout") {},
                    filteredItems:each(function(item, index)
                        return Rex("TextLabel") {
                            Text = item,
                            key = item
                        }
                    end)
                }
            }
        }
    }
end

Virtualization for Large Lists

For very large lists (1000+ items), consider virtualization:

local function VirtualizedList()
    local allItems = Rex.useState(generateLargeDataset()) -- 10,000 items
    local scrollPosition = Rex.useState(0)
    local itemHeight = 50
    local visibleCount = 20
    
    local visibleItems = Rex.useComputed(function()
        local startIndex = math.floor(scrollPosition:get() / itemHeight) + 1
        local endIndex = math.min(startIndex + visibleCount - 1, #allItems:get())
        
        local visible = {}
        for i = startIndex, endIndex do
            table.insert(visible, {
                data = allItems:get()[i],
                originalIndex = i
            })
        end
        return visible
    end, {allItems, scrollPosition})
    
    return Rex("ScrollingFrame") {
        CanvasSize = UDim2.new(0, 0, 0, #allItems:get() * itemHeight),
        onCanvasPositionChanged = function(scrollFrame)
            scrollPosition:set(scrollFrame.CanvasPosition.Y)
        end,
        
        children = {
            visibleItems:each(function(item, index)
                return Rex("TextLabel") {
                    Text = `{item.originalIndex}: {item.data}`,
                    Size = UDim2.new(1, 0, 0, itemHeight),
                    Position = UDim2.new(0, 0, 0, (item.originalIndex - 1) * itemHeight),
                    key = tostring(item.originalIndex)
                }
            end)
        }
    }
end

Best Practices

1. Always Use Keys

-- ✅ Good
items:each(function(item, index)
    return Component { data = item, key = item.id }
end)

2. Keep List Functions Pure

-- ✅ Good - pure function
items:each(function(item, index)
    return createListItem(item, index)
end)

-- ❌ Avoid - side effects in render function
items:each(function(item, index)
    if item.special then
        someGlobalState:set(true) -- Side effect!
    end
    return createListItem(item, index)
end)

3. Use LayoutOrder for Ordered Lists

items:each(function(item, index)
    return Rex("Frame") {
        LayoutOrder = index, -- Preserve order
        key = item.id
    }
end)

4. Memoize Expensive Operations

local processedItems = Rex.useComputed(function()
    return allItems:get():map(function(item)
        return expensiveProcessing(item) -- Only runs when allItems changes
    end)
end, {allItems})

return processedItems:each(function(item, index)
    return ItemComponent { item = item, key = item.id }
end)

Rex’s :each() method and intelligent reconciliation make it easy to build performant, interactive lists that scale from simple todo apps to complex data-driven interfaces.