Coding in Roact / Otter for the uninitiated

Introduction

I’m writing this guide because I’ve seen a lot of confusion or lack of knowledge of Roact - as well as a few misconceptions! Also, whilst the Roact docs (Roact Documentation) are helpful for getting started, they’re definitely written as more of a reference than a “101 for Roact” - and they don’t go into much detail about why you might want to use Roact in your projects!

What is Roact?

Roact, to simplify it, is a neat way of creating User Interfaces with nothing but code. It’s based heavily on the Web framework React by Facebook, with the same principles and code semantics (except in Lua, rather than JS, obviously).

It’s also a way of creating dynamic user interfaces. It’s sort of the “best of both worlds” of the two modes of data-driven user interfaces:

  • Retained Mode - where you have an object-based UI which you can then update by setting its properties. Example:

    textLabel.Text = "Hello, cruel world!"
    
  • Immediate Mode - where you update your user interface every frame to ensure it’s always up to date. Example:

    local function updateUI()
       textLabel.Text = ("Hello world, it has been %d seconds since unix epoch!"):format(tick())
    end
    RunService:BindToRenderStep("UpdateUI", Enum.RenderPriority.First.Value + 1, updateUI)
    

Both of these have upsides and downsides:

  • Immediate Mode has real performance issues, especially when poorly optimised - and some other things can be tricky to do too (animations, for example)
  • Retained Mode is best for simple applications and data forms, such as currency, unformatted strings, etc, but really makes things tricky when you’re trying to sync data accurately to the user interface and do processing on it too.

The idea of Roact was to introduce a dynamic UI framework, which behaves like a retained mode UI, but is actually an immediate mode UI.

Here is a really good talk by the main person behind Roact, @LPGhatguy, at RDC 18 - describing it much better than I could!

(If you’re just interested in Roact, skip to 4:42)

Why should I use it?

Well, there are the reasons why it was made, and some additional reasons as to why you should use it.

Before I go on, there is one thing you should be aware of: state
State, to really simplify, is just a fancy word for what it’s doing right now. For example, if I am going to the shops, my state is walking (or transporting). It’s used to refer to the knowledge of how an application is functioning. So, for example, if you’re programming a gun you could have state properties such as ammo, reloading, movementState (walking, crouching, etc). Don’t let the fancy word scare you!

I’m sure you’ve come across any of the following problems when developing UI:

  • What do you do if you want a consistent UI style (i.e. same theme of buttons, fonts, palettes), and want to make a change to how your buttons look or function at the same time?

  • What do you do if you want UI to be aware of the state of other UI? For example, you want to disable your custom Leaderboard when you enter the menu.

  • What do you do if you want to link your UI into a different system? For example, let your Gun UI know about the Leaderboard UI’s state.

If you’ve encountered these, then Roact could well be your solution! The way Roact works is by letting you define the structure of your interfaces, and then behind the scenes it creates it for you. There are a lot of powerful features it has that allow you to spend minimal work updating the structure of your code.

Getting Started in Roact

First you need to install Roact. This is dealt with well in the Roact guide, however the ideal environment is one where you use Rojo and keep that installed.

Then, you should make a new place file, copy the Roact module into ReplicatedStorage, create a new LocalScript somewhere and use this code:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local Roact = require(ReplicatedStorage.Roact) -- (1)

local app = Roact.createElement("ScreenGui", {}, { -- (2)
    HelloWorld = Roact.createElement("TextLabel", { -- (3)
        Size = UDim2.new(0, 400, 0, 300), -- (4)
        Text = "Hello, Roact!"
    })
})

Roact.mount(app, Players.LocalPlayer.PlayerGui) -- (5)

Hello, Roact! - Roact Documentation (source)

Let’s go through what this code does. I’ve numbered it:

  1. Importing the Module (needs no further explanation, though a quick tip - if you don’t want to type Roact.createElement every time, use the following line in your codebase:

    local e = Roact.createElement -- this defines it as "e"
    
    -- example usage:
    local app = e("ScreenGui", {}, { 
     HelloWorld = e("TextLabel", {
         Size = UDim2.new(0, 400, 0, 300),
         Text = "Hello, Roact!"
       })
    })
    
  2. Create your application element - remember with Roact, you need to define your entire UI in a top-down structure for it to be made. In Roact, data flows downwards and you can’t set the Parent of instances inside the element tree, so you need to think about your UI holistically (as in, think about it from the top and then move downwards). Each instance in your Roact tree is known as an Element

  3. Create Sub-Elements - the argument structure of Roact.createElement is as follows:

    Roact.createElement(ClassName, { Properties... }, { Child Elements... })
    
  4. Assign Properties to things - as detailed in (3), the second argument of the Roact.createElement function is a dictionary of all the properties. There are a few limitations here:

    • You can’t assign to the Parent property, as Roact is a tree structure, and you can’t change parents of elements on-the-fly. Your UI structure is “immutable” (simplified: unchangeable, however you can still modify properties and even if you render that element at all - we’ll come onto that)
    • If you want to listen onto Events, such as GuiButton.MouseButton1Click, then you need to use Roact Events
  5. Mount it! - this tells Roact that, under the folder you specify (PlayerGui, in this example), it will render the Element tree that you have given it. If you want to unmount (remove) it, just call Roact.unmount(app).

There you go! That’s the basics of Roact down. If you want to have a better read of the technical concepts, and begin getting more advanced - we’re not going to go into that here, but you should read on through the Roact documentation as that will help you further :slight_smile:

Here is a really basic “cheat sheet” to things in Roact, and what you might use them for though:

  • Components are when you want to have a distinct, (often) reusable element, such as a button, or particular UI element that is self contained. It’s often a good idea to have your entire UI based inside one Roact app, as this will make it easier for them to share state information!

  • Roact Events are ways for you to listen onto UI Events in a way you would have done previously with Roblox script connections (:Connect(), etc), whilst staying in the element tree. So, if you want to get a MouseButton1Click event, then you’d use a Roact Event

  • Fragments are ways of “grouping” UI together into separate units, but that container that they’re under is not actually present when the UI is mounted. For example, if you wanted a UIListLayout and then a group of elements, however you wanted the group of elements to be affected by the UIListLayout.

  • Portals are a bit of a cheat, to bypassing the “you can’t change parents from down in the tree!” issue, for example (as it lists in the Roact documentation) full-screen modals prompted by buttons. It can also be used to create / interact with stuff outside of your element tree. Use with caution!

  • Bindings are ways to change properties of your elements live. If you reference a binding, then whenever your binding is updated, anywhere you referenced that binding will be updated too. This is very cool! and one of the most useful features of Roact. One prime example is its use with Otter, an (unreleased) animation library by Roblox.

  • Refs are sort of the opposite of Bindings (in a simplified manner) - instead of updating a property inside Roact based on running code, you can reference Roact elements & properties from running code.

  • Context is a powerful way of having some property sent down your element tree without it having to be sent down as a property each step.

Getting Started in Otter

Otter is an animation library, designed by Roblox (although technically unreleased), which is primarily developed to work with Roact interfaces.

The principles are simple. You create motors, which have goals (what you want to animate to), and springs (the way you animate it).

Installing Otter is a bit more tricky - it isn’t a public repository yet, so you’ll need to dig into Roblox’s Packages to actually get it. This is quite easy with CloneTrooper1019’s Client Tracker:

  1. Go to https://github.com/CloneTrooper1019/Roblox-Client-Tracker
  2. Download it as a Zip
  3. Navigate to “LuaPackages/Packages/_Index/roblox_otter/”
  4. Move the roblox_otter folder to a Rojo project file
  5. Rename it to Otter
  6. Use Rojo to make it in Studio!

:warning: For anyone who doesn’t use the Rojo development environment, I’ve uploaded it for you! Otter.rbxmx (33.3 KB)

Please be aware this version won’t be updated, and if you want to keep updated you’ll need to use Rojo & GitHub :slight_smile:

To begin using Otter, here is some example code in their Documentation: https://roblox.github.io/otter/usage/motors/#demo-following-the-mouse

It’s really simple to get to grips with. What is slightly less simple is working out how to use it with Roact!

As you can read here: Roact - Otter Documentation, the way you are meant to do it is via Roact bindings. But if you’re not familiar with using Roact already, then this can be a bit confusing! (as it was to me).

Here is some code to help you understand (hopefully):

:warning: This Code probably won’t run for you, and even if it did, I’ve gutted all of the logic as the root script is 169 lines long. It’s just for an illustration!

local SpeakerComponent = Roact.Component:extend('SpeakerComponent')

function SpeakerComponent:init()
    self.singleMotor = Otter.createSingleMotor(1)

    self.motorBind, self.updateMotor = Roact.createBinding(1) -- Create the Motor binding

    self.singleMotor:onStep(function(value) -- Connect the Motor to the Binding
        self.updateMotor(value)
    end)
end

function SpeakerComponent:render()
    if not self.props.speaker or not self.props.speaker.speaker then 
        self.singleMotor:setGoal(Otter.spring(1)) -- Set the Goal
    else
        self.singleMotor:setGoal(Otter.spring(0)) -- Set the Goal
    end
    return Roact.createElement("ImageLabel", {
		Name = "Frame",
		ImageColor3 = Color3.fromRGB(32, 32, 32),
        ScaleType = Enum.ScaleType.Slice,
        ImageTransparency = self.motorBind, -- ImageTransparency is bound to the motor
		SliceCenter = Rect.new(100, 100, 100, 100),
		SliceScale = 0.25,
	}, {
		Circle = Roact.createElement("ImageLabel", {
			AnchorPoint = Vector2.new(0.25, 0.25),
			ImageColor3 = Color3.fromRGB(32, 32, 32),
			BackgroundTransparency = 1,
            Size = UDim2.new(0, 100, 0, 100),
            ImageTransparency = self.motorBind, -- ImageTransparency is bound to the motor
			Image = "rbxassetid://200182847",
			ScaleType = Enum.ScaleType.Fit,
		}, {
			Username = Roact.createElement("TextLabel", {
				BackgroundColor3 = Color3.fromRGB(32, 32, 32),
				BackgroundTransparency = 1,
				Position = UDim2.new(1, 15, 0, 30),
				Size = UDim2.new(2, 0, 0, 30),
				Font = Enum.Font.GothamBold,
				Text = self.props.speaker and self.props.speaker.username or "",
                TextTransparency = self.motorBind, -- TextTransparency is bound to the motor
			}),
			Rank = Roact.createElement("TextLabel", {
				BackgroundColor3 = Color3.fromRGB(32, 32, 32),
				BackgroundTransparency = 1,
				Position = UDim2.new(1, 15, 0, 50),
				Size = UDim2.new(2, 0, 0, 20),
                Font = Enum.Font.Gotham,
                TextTransparency = self.motorBind, -- TextTransparency is bound to the motor
			}),
			Icon = Roact.createElement("ImageLabel", {
				AnchorPoint = Vector2.new(0.5, 0.5),
				BackgroundTransparency = 1,
				Position = UDim2.new(0.5, 0, 0.5, 0),
                Size = UDim2.new(0, 65, 0, 65),
                ImageTransparency = self.motorBind, -- ImageTransparency is bound to the motor
		})
	})
end

Hope this helps people, as I wrote this as if it were for me a few months ago!

If you want further help, the best group of smart people to help you can be found in the Roblox Open Source Community Discord (click to join!)

70 Likes

Am ready for this, definitely sounds cool :eyes:

1 Like

Ah it seems like a try but i don’t know if its worth the hassle for my games because i use imagelabels and buttons but will be cool for other projects :eyes: