Video Version
Absurdly Long Text Version
If you’re looking for an advanced way to create your user interfaces, Roact might be the tool for you. Today I’ll be going over what it is, why you might want to use it, and how to use it.
Contents
- What is Roact?
- Why Roact?
- Elements
- Components
- Function Components
- Stateful Components
- Props
- Events
- Change Events
- Bindings
- Mappings
- State
- Refs
- Portals
- Fragments
- Context
- Callbacks
- Conclusion
What is Roact?
Roact is a declarative Lua UI library similar to Facebook’s React. Yeah, I stole that opening phrase from the documentation, what of it?
Essentially, by using Roact, you program UI elements from the ground up as opposed to fabricating them in StarterGui beforehand. A typical project includes building a tree of modules, each with their own instructions on how to display its particular component. This information is then passed to the Roact library, which mounts it on the PlayerGui and does its best to reconcile what the player sees with what you’ve described.
Why Roact?
Why would I go through the effort of programming all the properties of my interface line-by-line and learning this complicated framework when I have a perfectly functional graphical editor built into Studio? That’s essentially what my opinion of Roact was before I started using it for myself. However, as you work on bigger and more complicated projects, the perks of Roact begin to reveal themselves.
The core function of Roact itself is taking a description of what you want and drawing it on the screen. Describing what you want can take a long time, yes, but the rest of the development process can be dramatically simplified thanks to the declarative nature of Roact. That’s the second time I’ve used that word I don’t understand, so let’s define that. The concept of declarative programming is centered around describing what the program should do, as opposed to how to do it – the how is what Roact manages.
There are other neat perks to Roact, such as the reusable nature of the individual components – not just their appearance, but their functionality as well. Having your interface based entirely on code as opposed to objects also makes cooperating with other developers through Git and Rojo a hundred times better, since the whole team doesn’t constantly have to make sure they’re using the latest set of UI elements lest everything break.
Elements
Okay, I’ve done my best to explain these fairly abstract concepts, but you can only learn so much before applying anything, so let’s get into an example.
First, we’ll make an element with the createElement
function of the Roact library, which we’ll assume we’ve placed into ReplicatedStorage:
local Roact = require(game.ReplicatedStorage.Roact)
local interface = Roact.createElement("ScreenGui",{
Name = "Interface";
})
This call creates a ScreenGui and sets its Name property to “Interface”, since the first argument is the type of element we want to create and the second argument is a dictionary of properties, or props. “Element” is the term used to refer to an individual object created by Roact.
We can also add a third parameter in the form of a dictionary or table to assign descendants to the element. Let’s add a TextLabel with the text “Hello, Roact!” inside of the ScreenGui. To give a specific name to a child element, we’ll need to set the element’s key in the dictionary of children equal to whatever we want its name to be.
local Roact = require(game.ReplicatedStorage.Roact)
local interface = Roact.createElement("ScreenGui",{
Name = "Interface";
},{
Label = Roact.createElement("TextLabel",{
Text = "Hello, Roact!";
Size = UDim2.new(0,200,0,50);
})
})
Finally, we can load our interface into the game through the mount
function, passing through the complete element tree and the place we want to mount it. This simple example should give you a good idea of how to describe what you want and display it through Roact.
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local interface = Roact.createElement("ScreenGui",{
Name = "Interface";
},{
Label = Roact.createElement("TextLabel",{
Text = "Hello, Roact!";
Size = UDim2.new(0,200,0,50);
})
})
Roact.mount(interface,playerGui)
Components
However, describing individual elements in a tree is just the tip of the iceberg, and the true advantages of Roact begin to reveal themselves with components. Components take many forms, but they are best considered as different classes of elements that support custom behavior. The simplest component is a host component, which is simply a standard Roblox ClassName. We used these in our previous example, where ScreenGui and TextLabel were our components.
Function Components
The next type of component is a function component, which has the ability to be defined by the developer. A function component is simply a function that receives a dictionary of props, which can be used in any manner, and then returns any objects to add to the tree.
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
function Interface(props)
return Roact.createElement("ScreenGui",{},{
Label = Roact.createElement("TextLabel",props,props[Roact.Children])
})
end
local interface = Roact.createElement(Interface,{
Size = UDim2.new(0,200,0,50);
Text = "Hello, Roact!";
})
Roact.mount(interface,playerGui)
This example accepts a list of props and then assigns them to the TextLabel. It’s worth noting that, for custom components, it’s up to you to embed its children properly. You can access a component’s children through the props dictionary using the Roact.Children
key.
Stateful Components
The most powerful component is the stateful component. They’re more advanced, but allow for significantly more functionality, with support for powerful features like state, which we’ll discuss later. For now, let’s focus on the lifecycle methods by stepping through a simple example.
First, we need to create our component by extending the Component
class included with Roact. Let’s call it “Greeting” and pass the name as an argument to the extend
function for debugging purposes.
local Roact = require(game.ReplicatedStorage.Roact)
local Greeting = Roact.Component:extend("Greeting")
Next, we’ll make the render
method, which is the heart of the component. This method uses the props provided in the object’s state to provide elements to add to the tree. Let’s create a TextLabel, using the name prop we’ll pass to the component. This component can now be used to create a TextLabel with the provided name:
local Roact = require(game.ReplicatedStorage.Roact)
local Greeting = Roact.Component:extend("Greeting")
function Greeting:render()
return Roact.createElement("ScreenGui",{
Name = "Interface";
},{
[self.props.name] = Roact.createElement("TextLabel",{
Size = UDim2.new(0,200,0,50);
})
})
end
For its extensive functionality and fairly simple implementation, we’ll be using stateful components for the rest of the examples.
It’s typical for individual components to be placed into their own ModuleScripts for ease of organization, where each module just returns the component:
Greeting.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Greeting = Roact.Component:extend("Greeting")
function Greeting:render()
return Roact.createElement("ScreenGui",{
Name = "Interface";
},{
[self.props.name] = Roact.createElement("TextLabel",{
Size = UDim2.new(0,200,0,50);
})
})
end
return Greeting
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Greeting = require(script.Parent:WaitForChild("Greeting"))
local interface = Roact.createElement(Greeting,{
name = "TestName"
})
Roact.mount(interface,playerGui)
Props
We’ve already used props a fair bit, but let’s go into more detail. With host components, provided props should only be key-value pairs that correspond to existing properties of the Roblox instance being created. However, when we step into developer-defined components, props simply become a way to send arguments to the component. For instance, you could make the type of element created dependent on a prop, as shown in this example:
RandomComponent.lua
local Roact = require(game.ReplicatedStorage.Roact)
local RandomComponent = Roact.Component:extend("RandomComponent")
function RandomComponent:render()
return Roact.createElement(self.props.className)
end
return RandomComponent
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local RandomComponent = require(script.Parent:WaitForChild("RandomComponent"))
local interface = Roact.createElement(RandomComponent,{
className = "ScreenGui"
})
Roact.mount(interface,playerGui)
Events
Handling events with Roact is incredibly simple. The event and handler are passed as a key-value pair in the props dictionary, just like any normal property. Events should be referenced through the Event
dictionary included with Roact. In this case, I can detect when the button is clicked with Roact.Event.Activated
:
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:render()
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
[Roact.Event.Activated] = function()
end
})
end
Next, I’ll write a function to handle the event. Handlers should expect to receive the actual underlying Roblox instance associated with the event, followed by any parameters typically provided by the event. The Activated
event provides an InputObject and a click count, so I’ll include those and print the click count.
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:render()
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
[Roact.Event.Activated] = function(object,inputObject,clickCount)
print(clickCount)
end
})
end
return Button
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(Button)
})
Roact.mount(interface,playerGui)
Change Events
Roact also has its own variation of the GetPropertyChangedSignal
function to detect changes to specific properties of an instance. Its syntax is essentially identical, except the property name should be used instead of the event name, and it should be used to index the Change
dictionary instead of the Event
dictionary. The handler will only receive the instance that changed.
TextBox.lua
local Roact = require(game.ReplicatedStorage.Roact)
local TextBox = Roact.Component:extend("TextBox")
function TextBox:render()
return Roact.createElement("TextBox",{
Size = UDim2.new(0,200,0,50);
[Roact.Change.Text] = function(object)
print(object.Text)
end
})
end
return TextBox
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local TextBox = require(script.Parent:WaitForChild("TextBox"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(TextBox)
})
Roact.mount(interface,playerGui)
Bindings
So, what can we do with event handlers? Well, for one, we can modify the component’s appearance with bindings. We mentioned lifecycle methods before, and this is a case where one of those methods will come in handy. The init
method runs when the component is first mounted, which makes it the best place to define things like bindings. Let’s do that.
For this example, we’ll create a TextButton that displays the click count of the last activation, so we’ll want a binding to hold the latest click count. We can create a binding with the createBinding
function, providing a default value. In our case, we can just set it to 0. The createBinding
function actually provides two results: the binding itself, and a function to execute it. It’s standard practice to place both of these into the component itself, with the binding being given a normal name and the update function being called “update” with the name after that.
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self.clickCount, self.updateClickCount = Roact.createBinding(0)
end
In our render method, we’ll create a simple TextButton element and set its text equal to the clickCount
binding. Now, whenever the clickCount
is updated, the UI element’s property will automatically adjust itself to reflect the change. Let’s actually update the binding by bringing in our previous event example and calling the updateClickCount
function when the button is activated. The update function expects to receive a new value for the binding, and since we want to increment the clickCount
, we’ll need to get access to the value, not just the binding itself. We can do that with getValue
, which returns the raw value without the binding. This means we can add the clickCount
used in the activation to the existing clickCount
binding’s value and then pass this to the updateClickCount
function, automatically updating the button’s text to reflect the change.
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self.clickCount, self.updateClickCount = Roact.createBinding(0)
end
function Button:render()
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
Text = self.clickCount;
[Roact.Event.Activated] = function(object,inputObject,clickCount)
self.updateClickCount(self.clickCount:getValue()+1)
end
})
end
return Button
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(Button)
})
Roact.mount(interface,playerGui)
Mappings
But what if we want to add some text to the button besides the number? If we convert the raw value to a string and add it to a phrase, the property won’t update with the binding anymore. We need a way to mutate the binding in a way that doesn’t disconnect it, and that way is creating a map for the binding. Bindings have a method called map
that accepts a transforming function. The function itself receives the binding’s latest value and should return the modified value to be applied to the property. Now we can modify our binding before displaying it:
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self.clickCount, self.updateClickCount = Roact.createBinding(0)
end
function Button:render()
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
Text = self.clickCount:map(function(clickCount)
return "Click Count: "..tostring(clickCount)
end);
[Roact.Event.Activated] = function(object,inputObject,clickCount)
self.updateClickCount(self.clickCount:getValue()+1)
end
})
end
return Button
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(Button)
})
Roact.mount(interface,playerGui)
State
State is very similar to bindings, with only a couple differences, the main one being how it re-renders the entire element when it’s updated as opposed to just changing any bound props. This comes in handy for situations where your use of a variable proves to be more complex than just “put value here.”
Let’s suppose we have a situation where we want to show a completely different element depending on the current state. We’ll extend a new component and set the default state within the init
method using setState
, which accepts a dictionary of key-value pairs to write. If a value already exists in the state, it’ll be overwritten by the value passed here. If no value is passed, it will retain its previous version. For our purposes, let’s just create a flag to track which element should be visible, flipped on by default:
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self:setState({
elementFlag = true;
})
end
Now, in the render
method, we can check if the elementFlag
value is true
. If it is, we’ll return a TextButton that can be clicked. If it isn’t, we’ll just return a TextLabel with some text telling the user to be patient:
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self:setState({
elementFlag = true;
})
end
function Button:render()
if self.state.elementFlag then
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
Text = "Click me!";
})
else
return Roact.createElement("TextLabel",{
Size = UDim2.new(0,200,0,50);
Text = "Please wait...";
})
end
end
Let’s detect when the button is clicked and update the state to disable the flag. Then we’ll wait 2 seconds and flip it back. This will give us a TextButton that changes to a TextLabel for 2 seconds when we click on it.
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:init()
self:setState({
elementFlag = true;
})
end
function Button:render()
if self.state.elementFlag then
return Roact.createElement("TextButton",{
Size = UDim2.new(0,200,0,50);
Text = "Click me!";
[Roact.Event.Activated] = function()
self:setState({
elementFlag = false;
})
wait(2)
self:setState({
elementFlag = true;
})
end
})
else
return Roact.createElement("TextLabel",{
Size = UDim2.new(0,200,0,50);
Text = "Please wait...";
})
end
end
return Button
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(Button)
})
Roact.mount(interface,playerGui)
Refs
Sometimes it’s necessary to gain access to an element’s underlying Roblox instance, typically when assigning properties to other objects that expect instances as their properties. Refs are ideal for this. They’re very similar to bindings, both in terms of syntax and usage.
Let’s suppose we want to create a SurfaceGui that appears on a part we add to workspace
. To do this, we’ll need to set the SurfaceGui’s Adornee
property to the part, which means we need to create a ref pointing to the part in workspace
. We’ll start by using the createRef
function to define our ref in the init
method. The createRef
function doesn’t accept any arguments.
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = Roact.Component:extend("SurfaceGui")
function SurfaceGui:init()
self.partRef = Roact.createRef()
end
Next, in the render
method, we’ll create a Part in workspace
. To assign it to our ref, we’ll pass a prop through to it with the key of Roact.Ref
and the value of the ref we defined in init
. Then we’ll add a SurfaceGui as a child of the Part, and as we define its props, we’ll set its Adornee
to the ref.
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = Roact.Component:extend("SurfaceGui")
function SurfaceGui:init()
self.partRef = Roact.createRef()
end
function SurfaceGui:render()
return Roact.createElement("Part",{
Anchored = true;
Position = Vector3.new(0,10,0);
[Roact.Ref] = self.partRef;
},{
SurfaceGui = Roact.createElement("SurfaceGui",{
Adornee = self.partRef;
},{
TextLabel = Roact.createElement("TextLabel",{
Text = "Hello, Roact!";
Size = UDim2.new(1,0,1,0);
})
})
})
end
Much like bindings, you’ll need to call getValue
on the ref to get its actual value outside of use in props, though you should keep in mind that it’ll return nil
until the object is mounted:
--yes I know willMount() does not exist but it's just a demonstration
function SurfaceGui:willMount()
print(self.partRef:getValue()) --"nil"
end
function SurfaceGui:didMount()
print(self.partRef:getValue()) --"Part"
end
SurfaceGui.lua
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = Roact.Component:extend("SurfaceGui")
function SurfaceGui:init()
self.partRef = Roact.createRef()
end
function SurfaceGui:render()
return Roact.createElement("Part",{
Anchored = true;
Position = Vector3.new(0,10,0);
[Roact.Ref] = self.partRef;
},{
SurfaceGui = Roact.createElement("SurfaceGui",{
Adornee = self.partRef;
},{
TextLabel = Roact.createElement("TextLabel",{
Text = "Hello, Roact!";
Size = UDim2.new(1,0,1,0);
})
})
})
end
return SurfaceGui
Interface.client.lua
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = require(script.Parent:WaitForChild("SurfaceGui"))
local interface = Roact.createElement(SurfaceGui)
Roact.mount(interface,workspace)
this is impossible to read lol
Portals
Thing is, putting our SurfaceGui inside of the Part isn’t necessarily the best idea. It would make more sense to put it into the PlayerGui instead, but as you’ve probably caught on, the tree structure of Roact elements would make that difficult without mounting a completely separate Roact tree… and that’s where portals come in.
Portals make it possible to parent Roact elements under other objects outside of the tree. A portal acts as its own element, and the only prop it receives is a target
which serves as a new parent for any of the portal’s children. Any children added to the portal will appear under the portal’s target, so we’ll move our SurfaceGui to be under the portal.
SurfaceGui.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = Roact.Component:extend("SurfaceGui")
function SurfaceGui:init()
self.partRef = Roact.createRef()
end
function SurfaceGui:render()
return Roact.createElement("Part",{
Anchored = true;
Position = Vector3.new(0,10,0);
[Roact.Ref] = self.partRef;
},{
Roact.createElement(Roact.Portal,{
target = playerGui;
},{
SurfaceGui = Roact.createElement("SurfaceGui",{
Adornee = self.partRef;
},{
TextLabel = Roact.createElement("TextLabel",{
Text = "Hello, Roact!";
Size = UDim2.new(1,0,1,0);
})
})
})
})
end
return SurfaceGui
Interface.client.lua
local Roact = require(game.ReplicatedStorage.Roact)
local SurfaceGui = require(script.Parent:WaitForChild("SurfaceGui"))
local interface = Roact.createElement(SurfaceGui)
Roact.mount(interface,workspace)
Fragments
In this example, we have a Frame with a UIListLayout inside of it, and a component that provides labels to be shown in the frame. However, in order for the labels to be affected properly by the layout, they need to be under the same parent. We can only return one element through the labels component’s render method, though, so how do we find a workaround? Our best bet is to use fragments.
Fragments are useful for creating a component that returns multiple objects without wrapping them in another element. To make a fragment, simply use the createFragment
function, which accepts a dictionary of elements as its only argument. When we return the fragment, you can see that it now renders on the same level as the UIListLayout.
Labels.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Labels = Roact.Component:extend("Labels")
local contents = {
"hello world";
"owo wats this";
}
function Labels:init()
self.partRef = Roact.createRef()
end
function Labels:render()
local labels = {}
for index,content in pairs(contents) do
labels[index] = Roact.createElement("TextLabel",{
Size = UDim2.new(0,200,0,50);
Text = content;
})
end
return Roact.createFragment(labels)
end
return Labels
List.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Labels = require(script.Parent.Labels)
local List = Roact.Component:extend("List")
function List:render()
return Roact.createElement("Frame",{
Size = UDim2.new(0,400,0,200);
},{
UIListLayout = Roact.createElement("UIListLayout");
Labels = Roact.createElement(Labels)
})
end
return List
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local List = require(script.Parent:WaitForChild("List"))
local interface = Roact.createElement("ScreenGui",{},{
Roact.createElement(List)
})
Roact.mount(interface,playerGui)
Context
Previously we talked about passing information down to lower-level components through props, but this gets rather tiresome, particularly when it comes to nesting components. To simplify this process, we can use context.
Context can be thought of as a more complex version of state that spreads across multiple components. The first step is to create a Context
object with the createContext
function, which accepts a default value as its only parameter. The function returns a new context, which has a Provider
and a Consumer
class we can use later.
ThemeContext.lua
local Roact = require(game.ReplicatedStorage.Roact)
local ThemeContext = Roact.createContext({
backgroundColor = Color3.new(0.3,0.3,0.3);
textColor = Color3.new(1,1,1);
})
return ThemeContext
Later, in the component that needs access to the context information, we’ll wrap the contents of the render
method into a new element, passing the Consumer
of the context through as the ElementKind
. The only prop a Consumer
component requires is a render
function that receives the context information and returns the rest of the component like normal.
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local ThemeContext = require(script.Parent:WaitForChild("ThemeContext"))
local Button = Roact.Component:extend("Button")
function Button:render()
return Roact.createElement(ThemeContext.Consumer,{
render = function(theme)
return Roact.createElement("TextButton",{
Position = self.props.Position;
Size = UDim2.new(0,200,0,50);
BackgroundColor3 = theme.backgroundColor;
TextColor3 = theme.textColor;
})
end
})
end
return Button
In this example, I’m setting the background color and text color of the label equal to the theme settings defined in the default value of my context module. But what if I wanted to change the theme while the game was running, or only for a specific section of the Roact tree? This is where providers come in.
Let’s create a new component called SubTheme
. In its render
method, we’ll make a new element using the Provider
of our context as the ElementKind
. Providers only expect a single prop, which should be the new context value. We’ll also want to pass through the children of the component so they’ll actually render.
SubTheme.lua
local Roact = require(game.ReplicatedStorage.Roact)
local ThemeContext = require(script.Parent:WaitForChild("ThemeContext"))
local SubTheme = Roact.Component:extend("SubTheme")
function SubTheme:render()
return Roact.createElement(ThemeContext.Provider,{
value = {
backgroundColor = Color3.new(1,0.3,0.3);
textColor = Color3.new(1,1,1);
}
},self.props[Roact.Children])
end
return SubTheme
Now, we can surround components we want to affect with the different context with our SubTheme
component.
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local SubTheme = require(script.Parent:WaitForChild("SubTheme"))
local interface = Roact.createElement("ScreenGui",{},{
Button1 = Roact.createElement(Button,{
Position = UDim2.new(0,0,0,0);
});
SubTheme = Roact.createElement(SubTheme,{},{
Button2 = Roact.createElement(Button,{
Position = UDim2.new(0,0,0,50);
});
});
})
Roact.mount(interface,playerGui)
Callbacks
Alright, this part isn’t necessarily a feature of Roact, it’s more of a method, but it’s important to cover because it will make your life a whole lot easier. Everything we’ve discussed so far regarding the flow of information between components has revolved around passing info down the tree to sub-components. However, there’ll be a lot of times where you’ll want to pass information up the tree as well. Think of a close button component that needs to toggle the parent frame’s visibility.
In this scenario, it’s best to create your own updater function in the frame component and pass the function down to the button component as a prop. From the button component, you can hook up the button’s Activated
event to the updater function, which will change the frame’s visibility when the button is clicked. Heck, in this example, you don’t even need to create your own updater function – you can just create a binding for the frame’s Visible
property and pass the binding’s updater function down instead.
This may seem obvious to you, but I only just figured it out and man, it’s gonna save me a lot of headache down the line.
Button.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = Roact.Component:extend("Button")
function Button:render()
return Roact.createElement("TextButton",{
Size = UDim2.new(0,32,0,32);
Text = "X";
BackgroundColor3 = Color3.new(1,0.3,0.3);
[Roact.Event.Activated] = function()
self.props.callback()
end;
})
end
return Button
Frame.lua
local Roact = require(game.ReplicatedStorage.Roact)
local Button = require(script.Parent:WaitForChild("Button"))
local Frame = Roact.Component:extend("Frame")
function Frame:init()
self.visibility,self.updateVisibility = Roact.createBinding(true)
end
function Frame:render()
return Roact.createElement("Frame",{
Size = UDim2.new(0,400,0,250);
AnchorPoint = Vector2.new(0.5,0.5);
Position = UDim2.new(0.5,0,0.5,0);
Visible = self.visibility;
},{
Close = Roact.createElement(Button,{
callback = function()
self.updateVisibility(false)
end
})
})
end
return Frame
Interface.client.lua
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local Roact = require(game.ReplicatedStorage.Roact)
local Frame = require(script.Parent:WaitForChild("Frame"))
local interface = Roact.createElement("ScreenGui",{
Name = "Interface";
ResetOnSpawn = false;
},{
Frame = Roact.createElement(Frame)
})
Roact.mount(interface,playerGui)
Conclusion
There’s no perfect UI framework. It’s all about tradeoffs, and despite some limitations that’ll be annoying at times, I consider the organization Roact provides to be worth the extra time and effort. I suggest you try it as well and see if it’s right for you. Talk to your doctor about Roact today.
Now, Roact is really complex, and I didn’t have time to dive deep into every aspect of it. You’ll undoubtedly come across problems that even I don’t know how to solve. In that case, I’d highly suggest you check out a few sources that helped me out tremendously.
The first place to look when you’re stuck is obviously the Roact documentation, which is hosted on Github. The API reference especially does a great job of showing every tool at your disposal, and I’ve basically always got it open on a secondary monitor. Thing is, for questions that are more complex and less “what does X do,” just reading documentation won’t always get you far. In that case, the Roblox Open Source Community Discord server has a channel specifically for Roact that’s full of some really cool people with encyclopedic knowledge of the program who literally made Roact what it is. You can search for your question in there, because chances are someone’s asked it already. Otherwise, you can just ask it yourself. Links to the docs, Discord server, examples, and additional resources are listed below. This took forever to make, and I am going to go die now. Goodbye.
Resources
Here are some useful resources to learn more about Roact:
Example Files: https://github.com/ChipioIndustries/Roact-Tutorial
Download Roact: Releases · Roblox/roact · GitHub
Roact API Reference: API Reference - Roact Documentation
Roact Documentation: Roact Documentation
RDC18 Roact Talk: Roblox Developer Conference 2018 - Building Amazing GUIs - YouTube
Roact UI Animations: Coding in Roact / Otter for the uninitiated
Roblox Open Source Community Discord: Roblox OSS Community