Updating Conventions and APIs¶
Almost all legacy Roact code can be patched to Roact 17 using the RoactCompat library. However, while RoactCompat is backwards compatible with with Legacy Roact, it does not export new Roact 17 features like hooks.
When writing code exclusively for Roact 17, you should access the primary APIs of Roact 17 through the React
and ReactRoblox
APIs, instead of through RoactCompat
.
React
exposes:- some core legacy Roact APIs unchanged (
createElement
,createContext
,Component
,Change
/Event
, etc.) - some React-JS-aligned updates to other legacy Roact APIs (
createFragment
->Fragment
,oneChild
->Children.only
, etc.) - and several brand new APIs (
useState
,useRef
,memo
,cloneElement
, etc.)
- some core legacy Roact APIs unchanged (
ReactRoblox
exposes:- the
createRoot
API, which replaces themount
/update
/unmount
APIs - the
createPortal
API, which replaces theRoact.Portal
component
- the
To reduce confusion, try to avoid using RoactCompat
and React
/ReactRoblox
in the same file. If you are adding new Roact features to code that uses RoactCompat
syntax, take the following steps to align conventions:
- Replace the
Roact
import with theReact
import - Swap all uses of
Roact
withReact
- If the module uses any
Roact.Portal
instances, import theReactRoblox
package and replace them withReactRoblox.createPortal
- If the module uses
mount
/update
/unmount
, import theReactRoblox
package to replace them- Replace
Roact.mount
withReactRoblox.createRoot
followed byroot:render
- Replace
Roact.update
with subsequent calls toroot:render
- Replace
Roact.unmount
by callingroot:unmount
- Replace
- Begin adopting new features exported from the
React
package as needed
API Conversions¶
Roact 17 adopts the naming conventions and API shape of React JS wherever possible, with small deviations made to accommodate the distinct features of the Luau language. Using new naming conventions and APIs helps to align with the React JS ecosystem and reduce friction and mental overhead when troubleshooting or browsing documentation.
Even though Roact 17 is designed to be largely compatible with legacy Roact, updating these conventions when adopting new features is highly encouraged.
Mounting, Updating, and Unmounting¶
Legacy Roact uses three functions to manage the lifecycle of an entire Roact tree composed of numerous components. This is typically the entry point into Roact-managed UI.
In Roact 17, the equivalent mechanisms use a new concept called a "root" to more clearly encapsulate this behavior.
ReactRoblox.createRoot
ReactRoblox.createBlockingRoot
ReactRoblox.createLegacyRoot
Since the root needs to access a Roblox Instance
in order to attach to it, these functions are exported through the ReactRoblox
package, also known as a "renderer". A renderer takes the abstract descriptions of UI generated by React components and turns them into a concrete UI element tree. You can think of the ReactRoblox
package as the semantic equivalent of the react-dom
package in the React JS ecosystem.
Legacy Roact¶
local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
local Roact = require(Packages.Roact)
local roactTree = Roact.mount(Roact.createElement("TextLabel", {
Text = "Hello world!",
}, PlayerGui)
task.wait(3)
roactTree = Roact.update(roactTree, Roact.createElement("TextLabel", {
Text = "Hello Roblox!",
})
task.wait(3)
Roact.unmount(roactTree)
Roact 17¶
local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
local React = require(Packages.React)
local ReactRoblox = require(Packages.ReactRoblox)
-- Roact 17 roots will take full ownership of the instance provided to them,
-- so we should not create a root using PlayerGui directly
local container = Instance.new("Folder")
container.Parent = PlayerGui
local root = ReactRoblox.createRoot(container)
root:render(Roact.createElement("TextLabel", {
Text = "Hello world!",
})
task.wait(3)
root:render(Roact.createElement("TextLabel", {
Text = "Hello Roblox!",
})
task.wait(3)
root:render(nil)
Warning
The createBlockingRoot
and createLegacyRoot
functions will opt out of concurrent rendering, a feature of Roact 17 that allows smooth, scalable UI by dividing units of work across multiple frames when large amounts of UI changes are needed. We always recommend using createRoot
unless you know what you're doing.
Component Lifecycle Names¶
Legacy Roact used simplified names for component lifecycle functions to alleviate redundancy when defining lifecycle methods with syntax like function MyComponent:didMount() ... end
.
However, Roact 17 prefers to align as close as possible to React JS to reduce friction when referring to resources from the React JS ecosystem (questions, tutorials, examples, documentation, etc.). It's recommended to update class component lifecycle names to match React JS.
didMount
➡componentDidMount
shouldUpdate
➡shouldComponentUpdate
didUpdate
➡componentDidUpdate
didMount
➡componentDidMount
willUpdate
➡UNSTABLE_componentWillUpdate
- It's recommended to use the
UNSTABLE_
prefix here, since thecomponentWillUpdate
method is deprecated
- It's recommended to use the
willUnmount
➡componentWillUnmount
Reserved Keys¶
Legacy Roact went out of its way to let all prop keys be valid for component developers to use. In Roact 17, a small subset of prop keys are reserved for internal use to make them simple to provide.
[Roact.Ref]
becomesref
[Roact.Children]
becomeschildren
You can index props.children
to refer to the children provided to a component, and use ref
as a key to an element to provide a ref to it.
Legacy Roact¶
local FocusButton = Roact.Component:extend("FocusButton")
function FocusButton:init()
self.ref = Roact.createRef()
end
function FocusButton:render()
return Roact.createElement("Button", {
Size = self.props.Size,
[Roact.Ref] = self.ref
}, self.props[Roact.Children])
end
function FocusButton:didMount()
GuiService.SelectedObject = self.ref.current
end
Roact 17¶
local FocusButton = Roact.Component:extend("FocusButton")
function FocusButton:init()
self.ref = Roact.createRef()
end
function FocusButton:render()
return Roact.createElement("Button", {
Size = self.props.Size,
ref = self.ref
}, self.props.children)
end
function FocusButton:didMount()
GuiService.SelectedObject = self.ref.current
end
Context.Consumer¶
Legacy Roact treats the special Context.Consumer
component generated by createContext
more like a typical component with a single prop (render
) as its prop interface. React JS, on the other hand, uses a slightly abbreviated structure.
The Context.Consumer
component expects no props and a single child, where the child is the same mapping function that would be provided as the render
prop in legacy Roact.
Legacy Roact¶
local ThemeContext = Roact.createContext(nil)
-- ...
local function Button(props)
return Roact.createElement(ThemeContext.Consumer, {
render = function(theme)
return Roact.createElement("TextButton", {
BackgroundColor3 = theme.ButtonColor,
Text = props.text,
[Roact.Event.Activated] = props.onActivated,
})
end
})
end
Roact 17¶
local ThemeContext = Roact.createContext(nil)
-- ...
local function Button(props)
return Roact.createElement(ThemeContext.Consumer, nil,
function(theme)
return Roact.createElement("TextButton", {
BackgroundColor3 = theme.ButtonColor,
Text = props.text,
[Roact.Event.Activated] = props.onActivated,
})
end
)
end
Info
Roact 17 has a compatibility layer that will allow either structure to work as expected. If you'd like to see warnings that will help you migrate, enable the __COMPAT_WARNINGS__
global.
React.Fragment¶
Legacy Roact treats fragments as a special kind of rendered object, distinct from a Roact element, using the Roact.createFragment
function.
In Roact 17, however, fragments are nothing more than a special component type. Your component should return a single element with the React.Fragment
component type, with the children provided as the element's children.
Legacy Roact¶
local function LabeledButton(props)
return Roact.createFragment({
Button = Roact.createElement("ImageButton", {
Image = props.buttonIcon,
}),
Label = Roact.createElement("TextLabel", {
Text = props.labelText,
}),
})
end
Roact 17¶
local function LabeledButton(props)
return React.createElement(React.Fragment, nil, {
Button = React.createElement("ImageButton", {
Image = props.buttonIcon,
}),
Label = React.createElement("TextLabel", {
Text = props.labelText,
}),
})
end
ReactRoblox.createPortal¶
Legacy Roact uses a special Roact.Portal
component to represent a portal. In Roact 17, we align with upstream and export the ReactRoblox.createPortal
function.
Portals are considered to be a feature with renderer-specific functionality since they attach directly to a host instance. Any APIs that interact directly with Roblox Instances will be exported via ReactRoblox
in order to maintain the renderer abstraction.
Legacy Roact¶
local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
local function Modal(props)
return Roact.createElement(Roact.Portal, {
target = PlayerGui,
}, {
Modal = Roact.createElement("ScreenGui", {}, {
Label = Roact.createElement("TextButton", {
Text = "Click me to close!",
[Roact.Event.Activated] = props.onClose,
})
})
})
end
Roact 17¶
local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
local function Modal(props)
return ReactRoblox.createPortal({
Modal = Roact.createElement("ScreenGui", {}, {
Label = Roact.createElement("TextButton", {
Text = "Click me to close!",
[Roact.Event.Activated] = props.onClose,
})
})
}, PlayerGui)
end
Roact.oneChild¶
Legacy Roact provides an infrequently-used utility function called oneChild
that guarantees that the argument provided to it is a table with only one child Roact element. If the provided table contains zero elements or two or more elements, oneChild
will throw an error.
Roact 17 inherits a somewhat similar function from React JS: React.Children.only
. This function will throw an error if the provided argument is not itself a single child element. In other words, it expects a single child instead of a table containing only one child. This can be converted by unwrapping the provided children manually before passing them along to React.Children.only
.
Limiting a component to a single child isn't always necessary. Consider whether your component is capable of supporting an arbitrary number of children after all. In that case, you can simply remove the call to Roact.oneChild
.
Eliminating RoactCompat¶
Once you've completed all of the above conversions, you should no longer need to rely on the RoactCompat
package in your module. Congratulations, your migration is complete!