Roact: The Ultimate UI Framework

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?

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)

image


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)

image

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)

image


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)

image


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)

image
image


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)

image


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)

image


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)

image
image


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

image

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)

image
image


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)

image


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)

image


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: https://github.com/Roblox/roact/releases
Roact API Reference: https://roblox.github.io/roact/api-reference/
Roact Documentation: https://roblox.github.io/roact/

RDC18 Roact Talk: https://www.youtube.com/watch?v=vHTXAsHqrPI
Roact UI Animations: Coding in Roact / Otter for the uninitiated
Roblox Open Source Community Discord: https://discord.gg/85eegJn

50 Likes

Great tutorial! One of the best I’ve seen in this category! Very easy to understand - I will be constantly referencing this as I build a UI framework for my plugin and upcoming project.

4 Likes

I’ve been meaning to look into Roact. This tutorial should help in that considering how expansive it is and seeing that I’m moving towards Rojo-driven projects. I’m just remembering the nightmare it was converting a Gui into a *.model.json file for one project of mine - yes, by hand.

Thank you for the tutorial, very cool! I think I can finally get into the groove with it this way.

2 Likes

Quick note about the video: I’m aware of how fast it is. I actually intentionally tried to move slowly but I think I accidentally made it faster. For now, the pause button is your best friend, and you can use the written version to read along with the video.

3 Likes

You just made my life easier, Thank You!

1 Like

Awesome tutorial! I started using Roact in 2018 on one major game project and ended up going back to my original workflow because I felt like I was spending too much time learning to use it seriously and it slowed down my progress.

I do recommend using it for developers who work on larger teams mostly because cooperating with other devs is going to be way easier since you won’t have to constantly share files with the most up to date UI assets.

1 Like

Will be looking into this for my UI. I am trying to find a way to prevent my UI code ending up as spaghetti, and promote reusability in easier ways. Something I’m going to have to try as I move toward Rojo-based games!