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.
Stateful components in Roact are classes created by calling Roact.Component:extend("ClassName") (or Roact.PureComponent:extend). Each instance of such a class has access to self.props, self.state, and the methods documented on this page. Roact calls these methods at specific points in the component’s lifetime — understanding their order and restrictions is key to writing correct, performant UI.
Lifecycle Order
The following numbered sequences describe the order in which Roact calls component methods. Methods marked as static do not receive self.
Mounting (first render)
getDerivedStateFromProps(nextProps, {}) (static, if defined)
init(initialProps)
getDerivedStateFromProps(nextProps, state) (static, runs again after init)
render()
- (Roblox Instances are created)
didMount()
Updating (props or state change)
getDerivedStateFromProps(nextProps, lastState) (static, if defined)
shouldUpdate(nextProps, nextState) → if false, stop here
willUpdate(nextProps, nextState)
render()
- (Roblox Instances are patched)
didUpdate(previousProps, previousState)
Unmounting
willUnmount()
- (children are unmounted recursively)
- (Roblox Instances are destroyed)
Instance Methods
init
Component:init(initialProps)
Called exactly once when a new instance of the component is first created, before the initial render. Use init to set up the initial state and to store any non-rendering values (such as refs or bindings) directly on self.
You can establish initial state in two equivalent ways: calling self:setState(...) or assigning directly to self.state. Both are valid inside init — Roact treats them identically and will not schedule an extra render for either form.
Do not trigger side effects in init (network requests, event connections, Instance manipulation). Those belong in didMount, which runs after all Instances are ready.
The props table that the component was created with, after defaultProps merging has been applied.
local MyComponent = Roact.Component:extend("MyComponent")
-- Option 1: setState inside init
function MyComponent:init(initialProps)
self:setState({
count = 0,
label = initialProps.defaultLabel or "Hello",
})
-- Store a ref (not state) directly on self
self.frameRef = Roact.createRef()
end
-- Option 2: direct assignment to self.state (also valid in init)
function MyComponent:init(initialProps)
self.state = {
count = 0,
label = initialProps.defaultLabel or "Hello",
}
self.frameRef = Roact.createRef()
end
render
Component:render() -> RoactElement | nil
Describes what the component should display at the current moment. Roact calls render during the initial mount and after every accepted update. It must be defined on every component — the default implementation throws an error to prevent accidental omissions.
render must be a pure function. Its output must depend only on self.props and self.state. Do not read from external mutable state, call services, or produce side effects inside render. Roact may call render multiple times and expects consistent results.
If a component intentionally renders nothing, return nil explicitly. An implicit nil (falling off the end of the function) is also acceptable, but an explicit return nil makes the intent clear.
function MyComponent:render()
-- Correct: depends only on props and state
return Roact.createElement("TextLabel", {
Text = self.props.message,
TextColor3 = self.state.highlighted
and Color3.fromRGB(255, 220, 0)
or Color3.fromRGB(255, 255, 255),
})
end
-- Correct: intentionally renders nothing
function MyComponent:render()
return nil
end
-- WRONG: reads external mutable state
function MyComponent:render()
return Roact.createElement("TextLabel", {
Text = self.someExternalTable.value, -- not okay!
})
end
setState
Component:setState(stateUpdater | stateChange) -> void
Requests an update to the component’s state. Roact may resolve the update immediately or defer it for batching. The resulting state is shallowly merged — existing state fields not mentioned in the update are preserved.
Table form — pass a table whose keys are merged onto the current state:
function MyComponent:didMount()
self:setState({
loaded = true,
errorMessage = nil, -- nil has no effect; use Roact.None to delete
})
end
Function form — pass a function that receives the current pending state and current props, and returns a partial state table. Return nil from the function to abort the update entirely (no re-render is scheduled):
function MyComponent:didMount()
self:setState(function(prevState, props)
if prevState.count >= props.maxCount then
return nil -- abort: no update needed
end
return { count = prevState.count + 1 }
end)
end
Always use the function form when the new state depends on the previous state (e.g., incrementing a counter). Because setState may be asynchronous, the table form can read a stale self.state value.
To remove a key from state entirely, set it to Roact.None:
self:setState({ temporaryKey = Roact.None })
setState cannot be called from the following locations. Doing so will throw an error (or silently return, in the case of willUnmount):
willUnmount — the component is being torn down; state updates are meaningless.
willUpdate — an update is already in flight; a nested update would corrupt it.
render — render must be pure and side-effect-free.
shouldUpdate — same reason as render.
Calling setState inside init is special: the result is used as the initial state, and no additional render is scheduled.
A function (prevState: table, props: table) -> table | nil. Return a partial state table to merge, or nil to cancel the update.
A partial state table to merge onto the current state. Fields set to Roact.None are deleted.
shouldUpdate
Component:shouldUpdate(nextProps, nextState) -> bool
An optional hook that lets you short-circuit re-renders. Return false to prevent Roact from calling willUpdate, render, and didUpdate. Return true (or omit this method) to allow the update to proceed normally.
The default implementation always returns true. Roact.PureComponent overrides this with a shallow equality comparison of props and state.
The incoming props that the component would be updated with.
The incoming state that the component would be updated with.
Returns: boolean — true to proceed with the update; false to skip it.
function MyComponent:shouldUpdate(nextProps, nextState)
-- Only re-render if the text actually changed
return nextProps.text ~= self.props.text
or nextState.visible ~= self.state.visible
end
getElementTraceback
Component:getElementTraceback() -> string | nil
Returns the Lua stack trace captured at the point where the element for this component was created. This is useful for building diagnostic error messages that point directly to the Roact.createElement call site.
getElementTraceback only returns a non-nil value when the elementTracing option is enabled in Roact.setGlobalConfig. Without it, the method returns nil.
function MyComponent:validateSomething()
if not self.props.requiredValue then
error(
"requiredValue is missing! Created at:\n"
.. (self:getElementTraceback() or "<enable elementTracing>")
)
end
end
Lifecycle Methods
didMount
Component:didMount() -> void
Fired after the component’s initial render completes and all Roblox Instances in the subtree have been created. This is the first point at which it is safe to read from the Instance hierarchy or kick off side effects.
Good uses for didMount:
- Starting network requests or data fetches
- Connecting to Roblox events or services
- Modifying the Roblox Instance hierarchy (e.g., re-parenting non-Roact instances)
- Initiating animations or timers
function MyComponent:didMount()
-- Safe to access Instances here
local frame = self.frameRef:getValue()
print("Frame created:", frame:GetFullName())
-- Connect a service event
self.heartbeatConnection = game:GetService("RunService").Heartbeat:Connect(function(dt)
self:setState(function(prevState)
return { elapsed = prevState.elapsed + dt }
end)
end)
end
willUnmount
Component:willUnmount() -> void
Fired right before Roact begins unmounting the component’s children. Use this as the component’s destructor: disconnect events, cancel pending async operations, and release any resources acquired in didMount.
setState has no effect inside willUnmount. Any state changes are silently ignored because the component will be destroyed immediately after this method returns.
function MyComponent:willUnmount()
-- Disconnect any manually-connected events
if self.heartbeatConnection then
self.heartbeatConnection:Disconnect()
self.heartbeatConnection = nil
end
end
willUpdate
Component:willUpdate(nextProps, nextState) -> void
Fired after an update has been approved (by shouldUpdate) but before self.props and self.state are replaced. You can read the current props and state via self.props and self.state, and inspect incoming values through the arguments.
Do not call setState inside willUpdate. The component is mid-update and a nested state change will throw an error.
The props that will be applied after this method returns.
The state that will be applied after this method returns.
function MyComponent:willUpdate(nextProps, nextState)
if nextProps.theme ~= self.props.theme then
print("Theme is changing from", self.props.theme, "to", nextProps.theme)
end
end
didUpdate
Component:didUpdate(previousProps, previousState) -> void
Fired at the end of an update cycle, after self.props and self.state have been updated and all Roblox Instances have been patched. By this point, the UI reflects the new state of the component.
This is the recommended place for:
- Making network requests in response to prop changes
- Dispatching Rodux actions based on prop/state transitions
- Any side effect that must observe the final, committed state
Always compare the incoming previous values with the current values before triggering additional updates. Unconditionally calling setState in didUpdate will cause an infinite render loop.
The props that were in effect before this update.
The state that was in effect before this update.
function MyComponent:didUpdate(previousProps, previousState)
-- Only fetch when the search query actually changes
if self.props.searchQuery ~= previousProps.searchQuery then
self:fetchResults(self.props.searchQuery)
end
end
Static Methods
Static methods are defined on the component class table, not on instances. They do not receive self and must be pure functions.
getDerivedStateFromProps
static Component.getDerivedStateFromProps(nextProps, lastState) -> table | nil
A static lifecycle hook that runs before every render (both initial mount and updates). It receives the incoming props and the last committed state, and should return a partial state table to merge, or nil to make no change.
getDerivedStateFromProps runs before shouldUpdate. Any non-nil return value changes the state table, which means the new state will no longer be shallowly equal to the old one — this breaks PureComponent’s optimization.
This method is static — it has no access to self and cannot call instance methods. Keep it as a pure transformation from props to state.
Use getDerivedStateFromProps sparingly. In most cases, responding to prop changes in didUpdate is simpler and less error-prone. Avoid it if you are simply mirroring a prop into state as-is; just read from self.props directly in render instead.Like setState, you can return Roact.None as a field value to remove that field from the resulting state.
The incoming props that the component is about to receive.
The most recently committed state before this update.
Returns: table | nil — a partial state table to merge, or nil to leave state unchanged.
local Slider = Roact.Component:extend("Slider")
-- Clamp the internal value to the new min/max whenever props change
function Slider.getDerivedStateFromProps(nextProps, lastState)
local clamped = math.clamp(lastState.value, nextProps.min, nextProps.max)
if clamped ~= lastState.value then
return { value = clamped }
end
return nil
end
validateProps
static Component.validateProps(props) -> true | (false, string)
An optional static method for validating the props passed to a component. When propValidation is enabled in Roact.setGlobalConfig, Roact calls validateProps before every prop update (and before init on first mount). If validation fails, Roact throws a descriptive error.
Return true to indicate the props are valid. Return false and an error message string to indicate a failure. The message string is ignored when the first return value is true.
validateProps is static — it receives only the props table and has no access to self.
Property validation is disabled by default for performance reasons. Enable it during development with:Roact.setGlobalConfig({ propValidation = true })
Do not enable it in production environments.
The full props table that the component is about to receive, after defaultProps merging.
Returns: true if valid, or false, "error message" if invalid.
local Button = Roact.Component:extend("Button")
function Button.validateProps(props)
if type(props.label) ~= "string" then
return false, "Button requires a `label` string prop, got: " .. type(props.label)
end
if props.onClick ~= nil and type(props.onClick) ~= "function" then
return false, "Button `onClick` must be a function"
end
return true
end
function Button:render()
return Roact.createElement("TextButton", {
Text = self.props.label,
[Roact.Event.Activated] = self.props.onClick,
})
end
Static Properties
defaultProps
static Component.defaultProps: table
A dictionary of default values for props that callers have not specified. Before init and before every update, Roact merges defaultProps with the incoming props — incoming values always take precedence. This is equivalent to a component-level fallback table.
local Badge = Roact.Component:extend("Badge")
Badge.defaultProps = {
color = Color3.fromRGB(0, 120, 215),
size = UDim2.new(0, 80, 0, 24),
visible = true,
}
function Badge:render()
-- self.props.color, size, and visible are guaranteed to exist
return Roact.createElement("Frame", {
Size = self.props.size,
BackgroundColor3 = self.props.color,
Visible = self.props.visible,
})
end