Using Roblox-ts, Roact and JSX to create and manage UIs

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.

Back to the top


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):

Image

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!

Back to the top


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!

Back to the top


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:


Back to the top


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!

Back to the top


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.

Back to the top


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.

Back to the top


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:

Back to the top


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:

Image

(init is the constructor / function in Roblox-ts).

Back to the top


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!" />);

Back to the top


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 ¯\_(ツ)_/¯

Back to the top


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.

Back to the top


Resources for Roact

Back to the top


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!

38 Likes

Hey! Great tutorial and thanks for linking my converter plugin! Love the depth and code examples throughout.

If it’s of any use to you, I’ve recently updated my BasicState module to support Roact too. This can be used in place of Rodux, if you plan on creating any tutorials on global state management :grin:

2 Likes

Hello,

thanks for the helpful tutorial. I think it is a good addition to the tutorial on the roblox-ts website. Could you write another tutorial or article about which pattern to use for Ui development (Split everything into Data, Data representation (GUI) and data manipluation)? For example I would like to use the Model View Controller Pattern, but I don’t know how to implement it in this ecosystem. I am new to the TypeScript and Roact world (web development in general).

Thanks and greetings

1 Like

Generally speaking MVC is an OOP pattern and while you can do it with react/roact it’s generally not the way it’s done. In react we pass down our data through props, There’s a few types of components that you should know about (you can read about them here). React is also one directional in it’s data flow unlike MVC which is bi directional. But if you want to learn more about react and great patterns there is a mountain of articles online for everything you can imagine, one of the strong points of react is the about of support and community around it.

1 Like