Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Roblox/roact/llms.txt

Use this file to discover all available pages before exploring further.

The primary source of performance gains in a Roact application is reducing how much work the reconciler has to do on each update. By default, every state or prop change causes a component to re-render and Roact to diff its output against the previous virtual tree. For components that receive frequent updates — or that sit at the top of a large tree — this can add up quickly. Roact provides three complementary tools for cutting down that work: the shouldUpdate lifecycle method, the PureComponent base class, and stable child keys.

1. The shouldUpdate Lifecycle Method

When a component’s state or props change, Roact calls its shouldUpdate method to decide whether to proceed with a re-render. The default implementation on Roact.Component always returns true:
function Component:shouldUpdate(newProps, newState)
    return true
end
You can override this method in your own component to return false in cases where the new props and state don’t require a visual update. When shouldUpdate returns false, Roact skips the render call entirely and leaves the existing virtual tree in place.
local StatusBadge = Roact.Component:extend("StatusBadge")

function StatusBadge:shouldUpdate(newProps, newState)
    -- Only re-render when the status string itself changes
    return newProps.status ~= self.props.status
end

function StatusBadge:render()
    return Roact.createElement("TextLabel", {
        Text = self.props.status,
    })
end
The signature mirrors what you see in the source: shouldUpdate(newProps, newState) receives the incoming props and state as arguments, while self.props and self.state still hold the current values for comparison.
Manually implementing shouldUpdate is dangerous. If your comparison logic is incomplete or incorrect, components can silently stop updating when they should — producing UI that no longer reflects the true application state. This class of bug is particularly hard to debug because nothing throws an error; the screen simply shows stale data. In the majority of cases, use PureComponent instead. It provides a battle-tested implementation that covers the common shallow-equality case without requiring you to write comparison logic by hand.

2. PureComponent

Roact.PureComponent is an extension of Roact.Component that ships with a built-in shouldUpdate using shallow equality. Rather than always returning true, it compares each key in the incoming props against the current props (and checks whether the state reference changed) and only returns true when something is actually different. From the source, the full implementation is:
function PureComponent:shouldUpdate(newProps, newState)
    -- If the state reference changed, always update
    if newState ~= self.state then
        return true
    end

    -- If the props reference is identical, skip
    if newProps == self.props then
        return false
    end

    -- Shallow-compare each key in both directions
    for key, value in pairs(newProps) do
        if self.props[key] ~= value then
            return true
        end
    end

    for key, value in pairs(self.props) do
        if newProps[key] ~= value then
            return true
        end
    end

    return false
end
To use it, call Roact.PureComponent:extend instead of Roact.Component:extend:
local Item = Roact.PureComponent:extend("Item")

The Inventory Example

Consider the following Inventory and Item components. Inventory holds a list of items in its state and renders one Item element per entry:
local Item = Roact.Component:extend("Item")

function Item:render()
    local icon = self.props.icon
    local layoutOrder = self.props.layoutOrder

    return Roact.createElement("ImageLabel", {
        LayoutOrder = layoutOrder,
        Image = icon,
    })
end

local Inventory = Roact.Component:extend("Inventory")

function Inventory:render()
    local items = self.state.items

    local itemList = {}
    itemList["Layout"] = Roact.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder,
        FillDirection = Enum.FillDirection.Vertical,
    })
    for i, item in ipairs(items) do
        itemList[i] = Roact.createElement(Item, {
            layoutOrder = i,
            icon = item.icon,
        })
    end

    return Roact.createElement("Frame", {
        Size = UDim2.new(0, 200, 0, 400),
    }, itemList)
end
When a new item is added to self.state.items, Inventory re-renders and produces a new element for every Item — including ones whose props haven’t changed at all. With five existing items, adding one triggers six Item renders.
PureComponent pairs especially well with immutable data patterns such as those used with Rodux. Because immutability guarantees that a table reference only changes when its contents change, the shallow reference checks inside PureComponent:shouldUpdate are reliable. If you mutate a prop table in place rather than replacing it, the old and new references will be identical and PureComponent will skip the update — even though the data has changed. Always produce new tables when updating state or props.

3. Stable Keys for Child Elements

The third technique is about how you key the elements in a child list. By default it’s natural to use the loop index i as the key for each child element:
for i, item in ipairs(items) do
    itemList[i] = Roact.createElement(Item, {
        layoutOrder = i,
        icon = item.icon,
    })
end

The Problem with Numeric Index Keys

Suppose your inventory contains two items keyed by their position:
{
    { id = "sword",  icon = swordIcon  },  -- [1]
    { id = "shield", icon = shieldIcon },  -- [2]
}
Now a potion is inserted at the beginning:
{
    { id = "potion", icon = potionIcon },  -- [1]  ← new
    { id = "sword",  icon = swordIcon  },  -- [2]  ← shifted
    { id = "shield", icon = shieldIcon },  -- [3]  ← shifted
}
Because the keys are numeric indexes, Roact matches each key to the same underlying Roblox Instance as before. It now has to set Image on the existing ImageLabel at [1] to the potion icon, set Image on [2] to the sword icon, and create a brand new ImageLabel at [3] for the shield. Every existing Instance is touched even though the sword and shield didn’t actually change — they just moved.

Using Stable, Unique Keys

By using the item’s stable id field as the key instead, Roact can track each child across renders by identity:
for i, item in ipairs(items) do
    -- Use item.id as the key instead of the loop index
    itemList[item.id] = Roact.createElement(Item, {
        layoutOrder = i,
        icon = item.icon,
    })
end
With stable keys, Roact knows that "sword" and "shield" are the same elements as before — they haven’t been destroyed and recreated, they’ve just moved. The reconciler only needs to update their LayoutOrder properties to reflect their new positions. The Image property on those Instances is not touched at all. Only the new "potion" entry requires a fresh ImageLabel to be created with its Image set. Here is the full updated Inventory:render using stable keys:
function Inventory:render()
    local items = self.state.items

    local itemList = {}
    itemList["Layout"] = Roact.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder,
        FillDirection = Enum.FillDirection.Vertical,
    })
    for i, item in ipairs(items) do
        -- Each element is keyed by the item's stable unique id
        itemList[item.id] = Roact.createElement(Item, {
            layoutOrder = i,
            icon = item.icon,
        })
    end

    return Roact.createElement("Frame", {
        Size = UDim2.new(0, 200, 0, 400),
    }, itemList)
end
The Roblox Instance operations saved by stable keys compound as Item components grow more complex. If each Item renders several child elements itself, avoiding a full property-update sweep across all of them on every list change becomes increasingly valuable.

Applying the Techniques Together

The three techniques complement each other and can be layered in the same component tree:
1

Switch Item to PureComponent

Change Roact.Component:extend to Roact.PureComponent:extend on any component that receives props derived from immutable state. This alone eliminates redundant renders for unchanged items.
2

Key children by stable ID

Replace numeric loop indexes with a stable, unique identifier from your data (such as item.id). This prevents Roact from re-assigning Roblox Instance properties when list order changes.
3

Add shouldUpdate only when necessary

If a component has update conditions that shallow equality can’t capture — for example, it should skip updates unless a specific combination of props changes — implement a custom shouldUpdate. Keep the logic simple and test it thoroughly to avoid stale-render bugs.

Build docs developers (and LLMs) love