Introduction
As of late, I’ve been experimenting with Roact and have found it to be brilliant at creating beautiful UIs. Using it with Roblox-ts allows you to use JSX but customised for Roblox.
This is some seriously awesome stuff that I’d like to teach. Shall we begin?
Tutorial parts
Prerequisites
Software
Knowledge
-
TypeScript basics.
-
React basics, especially with using JSX.
-
Rojo and Roblox-ts usage.
Setup
Firstly, open your command line in your project folder (you can use VSCode’s integrated terminal) and run the init command for Roblox-ts. We want a game template:
$ rbxtsc init game
Now, wait for it to add all the necessary folders and files.
Once it’s created, you’ll want to edit the default.project.json (config file for Rojo). Redivert the client folder to be within StarterGui, like so:
{
"name": "roblox-ts-game",
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$className": "ServerScriptService",
"TS": {
"$path": "out/server"
}
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"rbxts_include": {
"$path": "include",
"node_modules": {
"$path": "node_modules/@rbxts"
}
},
"TS": {
"$path": "out/shared"
}
},
"StarterGui": {
"$className": "StarterGui",
"TS": {
"$path": "out/client"
}
}
}
}
You can choose to keep the other folders, they’re not needed for this tutorial.
Secondly, change the main.client.ts file in src/client to a tsx file (main.client.tsx):
This’ll allow you to use JSX.
Thirdly, you’ll want to install the Roact port for Roblox-ts to our project:
$ npm install @rbxts/roact --save
Optionally, a package for importing services easily can be installed:
$ npm install @rbxts/services --save
I would recommend installing this.
Forthly, start Rojo listening on the default port:
$ rojo serve
And connect it in Roblox Studio:
Fifthly, in another terminal set rbxtsc to watch mode so it’ll transpile immediately after a file is edited:
$ rbxtsc -w
Finally, give yourself a pat on the back. You’re ready to start programming!
Beginning with Roact
First and foremost, we’ll learn the very basics on Roact.
To access all the methods we need, import Roact:
import Roact from '@rbxts/roact';
We’ll also need Players service, either use game.GetService or import it:
import { Players } from '@rbxts/services';
If you’ve used React before, you’ll understand how via JSX you can create elements. ie. for a div:
<div />
This is just about the same for Roblox, ie. creating a ScreenGui:
<screengui />
(/ has to be used to close the element, this will error if not closed).
Now you’re probably wondering, how can we add properties and children? Simply define them like usual JSX:
const element = (
<screengui ResetOnSpawn={ false }>
<textlabel
Text="Hello World!"
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
/>
</screengui>
);
(You must utilise braces if you want to declare an expression that isn’t a string).
To the rending stage, we’ll firstly want to grab the PlayerGui:
const playerGui = Players.LocalPlayer.FindFirstChild("PlayerGui") as PlayerGui;
(the PlayerGui will exist because the LocalScript is running in it).
Then, pass the element to the mount method of Roact.
Roact.mount(element, playerGui, "UI");
mount takes the element, the Parent and a Name. Furthermore, our ScreenGui will be parented to the PlayerGui with the name of “UI”. mount returns a “handle” which can be updated (update method) or unmounted (unmount method). state is preferred instead of updating however, we will get to that later in the tutorial.
In total, your code should now look like:
import Roact from '@rbxts/roact';
import { Players } from '@rbxts/services';
const playerGui = Players.LocalPlayer.FindFirstChild("PlayerGui") as PlayerGui;
const element = (
<screengui ResetOnSpawn={ false }>
<textlabel
Text="Hello World!"
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
/>
</screengui>
);
Roact.mount(element, playerGui, "UI");
Now that we're done: save, go into Roblox Studio and play. You should see something like:
With just a few lines of code, you can create brilliant UIs. This is only the tip of the iceburg though!
Event handling
Of course, with UI you’ll want to detect events such as the mouse entering / leaving the interface. In all elements, there’s an Event property which can be assigned to an object of callbacks. Currently, we’ll just print but later on we’ll implement some cool color changing text!
For example with our TextLabel element:
const element = (
<screengui ResetOnSpawn={ false }>
<textlabel
Text="Hello World!"
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => print("Entered!"),
MouseLeave: () => print("Left!")
}}
/>
</screengui>
);
And as you can see, it works like a charm:
Creating components
One of the best parts of Roact is components, they can either be functional or stateful (uses class syntax) and have properties of their own. Components can be re-used infinitely just like elements and be named whatever you want.
For example, a functional version of our UI would be:
function UI() {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text="Hello World!"
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => print("Entered!"),
MouseLeave: () => print("Left!")
}}
/>
</screengui>
);
}
Moving onto stateful, they’re very similar but extend the Component class and have a ‘render’ method which is invoked when the component is about to be rendered:
class UI extends Roact.Component {
render() {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text="Hello World!"
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => print("Entered!"),
MouseLeave: () => print("Left!")
}}
/>
</screengui>
);
}
}
These can both be used in JSX like a normal element:
<UI />
As I previously mentioned, you can use custom properties for your components. However how do we access these? Well it’s actually really simple; for functional components they’re passed as the parameter and for stateful they’re a property therefore can be accessed in the render method via this.props. (props is the name given for component properties).
In this case, we’ll use a ‘text’ prop for our TextLabel’s Text. Henceforth, we’ll declare an interface for our props:
interface UIProps {
text: string
}
Then to ensure that the props gets this as it’s typing, assign it in the parameters for functional components:
function UI(props: UIProps) {
Or assign one of the generics for the React.Component which we extend:
class UI extends React.Component<UIProps> {
(there’s another generic for state but we’ll get to that later in the tutorial).
Now that’s done, you can integrate it into your component. For example in functional:
function UI(props: UIProps) {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ props.text }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => print("Entered!"),
MouseLeave: () => print("Left!")
}}
/>
</screengui>
)
}
And in stateful:
class UI extends Roact.Component<UIProps> {
render() {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ this.props.text }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => print("Entered!"),
MouseLeave: () => print("Left!")
}}
/>
</screengui>
);
}
}
Finally, use it as you please:
<UI text="Components are awesome!" />
Ta-da!
Using a component’s state
In stateful components, there’s a tremendously helpful property named state (hence the name) just like React and it’s class-based components. This can be updated via the setState method and used anywhere you need to in the component. When the state is updated, anything using the state is also updated accordingly. Unfortunately, this is only usable in stateful components.
As I said previously, it’s the second generic to the Component class and therefore we’ll need to create an interface for it. Here’s our chance to add the text color changing effect and thus we’ll use Color3:
interface UIState {
textColor: Color3
}
Then, assign the next generic:
class UI extends Roact.Component<UIProps, UIState> {
Afterwards, edit it so that the TextColor is assigned and the state is changed on hover. State may also want to be predefined ahead of usage. Overall, your code should now look like:
import Roact from '@rbxts/roact';
import { Players } from '@rbxts/services';
const playerGui = Players.LocalPlayer.FindFirstChild("PlayerGui") as PlayerGui;
interface UIProps {
text: string
}
interface UIState {
textColor: Color3
}
class UI extends Roact.Component<UIProps, UIState> {
state = {
textColor: new Color3()
}
render() {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ this.props.text }
TextColor3={ this.state.textColor }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => {
this.setState({
textColor: new Color3(0, 0, 1)
})
},
MouseLeave: () => {
this.setState({
textColor: new Color3()
})
}
}}
/>
</screengui>
);
}
}
Roact.mount(<UI text="Components are awesome!" />, playerGui, "UI");
Incredible.
Utilising component bindings
If you’ve used the React Hook useState, you’ll definitely love this.
Roact bindings are similar to a component state however they’re for one property and can be used within functional components. The createBinding method takes a initial value for the property and returns an array holding the binding value with a function to update it. Anything in Roact using the binding value as it’s value will be updated whenever changed - just like a property in state.
Let’s adjust our stateful component to a functional one now using bindings, just use the returned binding value for TextColor3 and call the returned update function instead of setState:
function UI(props: UIProps) {
const [textColor, updateTextColor] = Roact.createBinding(new Color3());
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ props.text }
TextColor3={ textColor }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => updateTextColor(new Color3(0, 0, 1)),
MouseLeave: () => updateTextColor(new Color3())
}}
/>
</screengui>
)
}
This works exactly the same as the stateful component using a state! If you want to, you can use bindings in class-nased component as well - the choice is up to you.
If you ever need to get the binding’s current value for yourself, the binding value has a getValue method. Additionally, there’s a map method if you ever need to adjust it when changed - the method takes a function with the parameter of the current value and should return the new value. For example, if we wanted to adjust the Hue of the Color slightly:
function UI(props: UIProps) {
const [textColor, updateTextColor] = Roact.createBinding(new Color3());
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ props.text }
TextColor3={textColor.map((value) => {
const [H, S, V] = Color3.toHSV(value);
return Color3.fromHSV(H + 0.1, S, V);
})
}
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Event={{
MouseEnter: () => updateTextColor(new Color3(0, 0, 1)),
MouseLeave: () => updateTextColor(new Color3())
}}
/>
</screengui>
)
}
Now it’s displayed as purple instead of blue.
Applying references to elements with refs.
Occasionally, you’ll want to use elements outside of Roact to, for example, read it’s Text. This is where refs come in. To firstly create a ref you must use the createRef method, then assign the Ref attribute in your element to the created ref. Now you can use the getValue method on your ref to grab the instance anytime.
Let’s go back to our stateful and create a ref property for a TextLabel, then assign the Ref attribute to it - this must be done in the constructor since it’ll be ignored if not. Last but not least in a didMount method of our component, we can connect to whenever the TextColor3 changes with a GetPropertyChangedSignal event. This method will be invoked on the initial mounting of our component and therefore the ref will exist. Like so:
class UI extends Roact.Component<UIProps, UIState> {
textLabelRef: Roact.Ref<TextLabel>
state = {
textColor: new Color3()
}
constructor(props: UIProps) {
super(props);
this.textLabelRef = Roact.createRef<TextLabel>();
}
render() {
return (
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ this.props.text }
TextColor3={ this.state.textColor }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Ref={ this.textLabelRef }
Event={{
MouseEnter: () => {
this.setState({
textColor: new Color3(0, 0, 1)
})
},
MouseLeave: () => {
this.setState({
textColor: new Color3()
})
}
}}
/>
</screengui>
);
}
didMount() {
const textLabel = this.textLabelRef.getValue() as TextLabel;
textLabel.GetPropertyChangedSignal("TextColor3").Connect(() => {
print("The TextLabel's color has changed to:", textLabel.TextColor3)
})
}
}
As expected, it prints whenever it changes:
Lifecycle methods for stateful components
Such as I just shown, there are certain methods which are invoked when a component enters a certain stage. This diagram from the official Roact documentation explains all:
(init is the constructor / function in Roblox-ts).
Building portals to outside instances
Thanks to portals, you can render a specific instance which is not managed by React by setting a ‘target’. No matter how far down a tree, the component is rendered within that target.
If we wanted it for our UI, wrap the screengui element in a Portal component and set the target to playerGui:
<Roact.Portal target={ playerGui }>
<screengui ResetOnSpawn={ false }>
<textlabel
Text={ this.props.text }
TextColor3={ this.state.textColor }
TextScaled={ true }
Position={ new UDim2(0.4, 0, 0.1, 0) }
Size={ new UDim2(0.2, 0, 0.1, 0) }
BackgroundTransparency={ 1 }
Ref={ this.textLabelRef }
Event={{
MouseEnter: () => {
this.setState({
textColor: new Color3(0, 0, 1)
})
},
MouseLeave: () => {
this.setState({
textColor: new Color3()
})
}
}}
/>
</screengui>
</Roact.Portal>
Now we can even call mount can not include the Parent argument:
Roact.mount(<UI text="Components are awesome!" />);
Producing fragments of multiple elements
Sometimes you might have multiple elements of your component but not underneath their own set Parent. Instead, they’re Parented to wherever in the tree they’re used in. Fragments have a similar implementation to Portals where you use the component in your JSX.
For instance, if we created a functional component which held two TextLabels in a fragment:
function TwoTextLabels() {
return (
<Roact.Fragment>
<textlabel />
<textlabel />
</Roact.Fragment>
);
}
And so parented it to a Frame in a ScreenGui:
<screengui>
<frame>
<TwoTextLabels />
</frame>
</screengui>
Then you’d get a Frame with two TextLabels when mounted ¯\_(ツ)_/¯
Tips for using Roact
A few helpful tips which I’d like to add:
-
There is no Name attribute on JSX elements, instead it’s ‘Key’. Key defaults to what number child it is.
-
Try to use stateful components with the state property or functional components and bindings instead of using the update method consistently. This reduces the amount of rendering.
-
Just like React, it’s a good idea to use seperate files for components and export / import them.
-
You don’t have to use JSX! Using the vanilla createElement method is all fine in Roact for TypeScript.
Resources for Roact
Thanks for reading!
This was my sixth Community Tutorial, hope you did enjoy.
If there’s anything you’d like to ask, or correct, do reply!