Rocrastinate: a Roact/Rodux-like Library Optimized for Roblox


Disclaimer: I will not explain the API of Rocrastinate, or its particulars in this post. Instead, I have a tutorial on GitHub explaining the particulars of Rocrastinate and how it works. I would also say that this library is for more advanced lua programmers. Please check out this link:

I just released a new UI library called Rocrastinate. Rocrastinate is a class-based approach to UI that offers similar declarative features to the roblox-endorsed libraries Roact/Rodux, but with much more versatility and optimization for the Roblox environment.

The core feature of Rocrastinate is that, unlike Roact, UI updates are “procrastinated” until a UI component absolutely needs to be rendered—i.e. during a RenderStepped or Heartbeat binding. Rocrastinate guarantees that a UI element will be rendered at most once per frame. This, I find, is more suitable for Roblox development than Roact, as we don’t need to reconcile a bunch of information every single time a portion of the state changes. We only need to do that just before we are displaying that information to the user; Roblox UI is bottlenecked by RenderStep, so I see no reason to handle these things asynchronously.

Finally, with my experience of using Roact, I believe it was too restrictive when working with UI templates. Roblox has a complex UI editor, and a great workflow for designing UI objects—why should this be thrown away? With Rocrastinate, you have full control over how your UI is created and rendered, including the ability to :Clone() from templates.

Here is the composite of all of the UI I designed for a Rocrastinate project. These are static templates which are :Clone()'d at runtime.


image

I made a plugin in order to test out the viability of the framework, and see if the workflow was one I liked. Overall, I can confidently say that this library offers a lot in terms of keeping code clean, re-usable, and scalable. I will be using Rocrastinate for my future projects.

Proof of Concept: Icon Creator plugin

Here’s the tree structure of the Plugin I created with Rocrastinate:

Here are some key points of understanding this structure:

  • You can see the modules ‘actions’ and ‘rootReducer’ —these are used for the Redux-like features of managing the plugin’s whole UI state
  • The structure is similar to a React app, but with the added benefit that each Component renders Gui Templates, rather than creating each object by script and going through the costly process of reconciliation. With Rocrastinate, the minimal amount of rendering is applied when a component needs to update.
  • You can see the plugin’s different portions are highly componentized.

The “App” module encapsulates the whole plugin while it is active.
The “PluginWindow” encapsulates the main draggable frame in which the plugin can be used.
From there, you have “PropertiesPanel” and “IconViewport” (the two main components within the window), and these are further subdivided into re-used components like “NumericInputBox”, “Slider”, and “BoolCheckbox”

Video Link (ignore the cursor offset; that’s just my screen recorder)

Plugin Link:
Icon Creator - Roblox

Place File:
https://raw.github.com/headjoe3/Rocrastinate/master/examples/icon_creator.rbxl

Check out the tutorial:

You can get the Rocrastinate library here:

This is just the initial release. For those who have used Roact in the past, I highly encourage you take a look at this library and see the results for yourself.

53 Likes

Impressive design. I can see potential for this to be used elsewhere. I just have a few concerns regarding the efficiency of using this. I own a higher end PC and not a lower end one so testing this is difficult. Can you confirm that this is efficient when running on lower end PC’s? I do not want to use something like this if it’s going to be laggy.

EDIT: By “laggy”, I mean laggy in-game, not in-studio.

2 Likes

“Efficient on lower-end PCs” is a difficult metric to benchmark

All I can say is that this framework is very lightweight and, like Roact, its optimization depends on how you use it. I would also say that, unlike Roact, this is as efficient as you can really get for having this kind of state management, and probably more efficient than no framework at all.

If you completely ignore the state management features (You can follow the tutorial to learn about what that is), then the framework will be about as efficient as any other framework or no framework at all.

If, however, you use the state management features (Again, read the tutorial), it will still be a lot more optimized than Roact, as state changes are only ever updated once per frame.

So it’s a bit nuanced, but from my understanding of how Lua works, and because the aim of this framework is primarily optimization, I would say this framework is ideal for low-end devices.

That being said, Roblox’s mobile site uses Roact, and this is definitely more optimized than Roact.

3 Likes

So this is sort of like lazy loading UI?

Lazy definitely describes this framework’s namesake. Although “Lazy-loading” can mean something else in programming (like, as a way of having modules require each other, which is not really good UI structure).

The biggest advantage is that it only renders once per frame, which is the minimum amount of time any UI element needs to be rendered. I will say that there is potential for one-frame-off errors, which you will need to debug yourself. If a component updates once in a frame, it will exhaust it ability to update any more within that same frame.

Here’s an example that may explain a bit more about how Rocrastinate works

Let’s say you have ComponentA which contains/updates an object of the class ComponentB when ComponentA updates.

If ComponentA has a redraw binding at “Heartbeat”, and ComponentB has a redraw binding at “RenderStep”, then everything should work fine, even if ComponentA calls some function in its Redraw function that queues a redraw for ComponentB.

The order in which Roblox displays elements is always HeartBeat -> RenderStep -> Actually render the object to the user. This is in a loop with no particular start/endpoint, so it’s a continuous cycle of Heartbeat -> RenderStep -> Render -> Heartbeat -> RenderStep -> Render -> etc.

Generally it’s the barrier between RenderStep and Render that you want to watch out for when it comes to one-frame-off errors. Rocrastinate does its best to prevent this, but there are still scenarios in which this can happen (e.g. if ComponentA is bound to RenderStep and ComponentB is bound to Heartbeat, then ComponentA will update before the Render of this frame, but ComponentB will only update before the Render of the next frame).

It is usually safe to have both components with the same binding (i.e. both update on Heartbeat), and have one queue a redraw in another, since Rocrastinate completes once all queued components have been redrawn, even of new components are added to the queue (but every component will update no more than once per frame)

1 Like

while i think initial concerns about roact and rodux’s preformance are valid, i’d like to point out that you make a lot of incorrect assumptions about how these systems work.

anecdotally, i can tell you that while i’ve had bumps learning roact initially, after a year of using it i would never use anything else for UI.
the single case i’ve ever encountered a preformamce problem with roact (scrolling through a large list of assets whose icons needed to be updated) we were able to easily integrate logic to create and the instances directly in the asset-list-component using refs and the components self-contained state.

taken from the github’s readme:

while the pattern for mutating objects (defining refs) is more verbose in roact, i wouldn’t say its at all awkward or difficult to use. if you find yourself constantly needing to manually overwrite properties outside of tweening, i don’t believe you’re fully utilizing the system as intended.
additionally, this has been made even easier in the newest version of roact, using bindings. tweens are being handled through bindings along with a new library called otter, which hasn’t had a public release yet, but you can still pull from the roblox corescripts.

this isn’t correct. only “parent” tables get re-assembled, when any “child” value they have is changed. for example, if you had three state slices in your main state, and you dispatched an action which only changed one of them, the value inside that one state slice would change, (any “sibling” data would stay the same), the one state slice would change (the other two would stay the same) and the state would change. this is what makes rodux handy, as you can iteratively run shallow comparisons on levels of the state, and only do a deeper comparison when you see that part of the state has changed. not only the act of recreating the entire state would be less preformant, finding differences between two state versions would be laborious for programmers and inefficient for the system.

i can’t personally say that this is false, but from what i’ve heard from engineers is that the distinction isn’t large enough to be meaningful in most use cases

this is completely incorrect. components only update when their properties are changed or when setState is used to manually trigger a state change and re-render. roact uses shallow equal operations on changed properties in components passed from render methods to minimize the amount of property updates per rerender

this is only the case when either using unstable keys for children or constantly passing a different component type using the same key (i.e. having a child “Frame” sometimes be a frame and sometimes a textlabel). both of these patterns are considered pitfalls and are easy to avoid. in very specific cases, as i mentioned earlier, direct control of certain UI elements is fairly painless to achieve, and even easier now with bindings.
on the matter of object pooling, the lua core team working on roact has investigated the usage of this and has found that it’s largely not useful for the types of workloads roact was designed to handle.

while i think your concern with many of roact and rodux’s problems are warranted, a lot of them seem to stem from a misunderstanding of the system and a lot of its common patterns. i don’t blame you for this, as roact is still in its infancy and many of its’ assistant tools and features which make it great have yet to get a public release or proper documentation. it’s hard for roblox to document and maintain these best-practice patterns and paradigms because they’re still changing and evolving so rapidly to meet new needs and solve newly discovered pain points and problems. there aren’t a lot of large scale open source projects that set a good precedent and if there were, they’d quickly become dated or contain old patterns without constant upkeep. ultimately though, i think roact will really shine in the long run as the lua core team is able to properly wrangle it and all of its tooling that makes it great, and present it to the community in a proper packaged format

12 Likes

on that same note, here 3 reasons why you should definitely use roact & rodux

long term support from roblox engineers

roact and rodux are being adopted as the standard for all of the in-engine UI development at the company. the entire roblox mobile app was completely written using roblox’s UI elements, lua, and roact/rodux. new tools and features are in constant development for these systems because internally, engineers are dependent on them. pain points will be located and ironed out, systems will be properly documented, and problems and bugs will be solved. you can expect roblox to be supporting these systems for a very long time.

transferable skills

roact and rodux aren’t new concepts, and are lua implementations of facebook’s react and redux frameworks. these systems are used EVERYWHERE in web development, and now as the line gets blurred between web and desktop development, they’re even used to create JS desktop apps like discord and visual studio code. many of the main concepts and patterns you’ll learn using roact and rodux are easily transferable to their JS counterparts, and being able to move from developing roblox UI to developing fully featured desktop apps is a valuable thing

it just makes ui way easier lol

i would never go back to developing UI any other way. if you want to make any UI more complex than a few buttons in 2019, this is how you should be doing it. had these paradigms been around roblox earlier, it would have saved me so many headaches. roact components are **incredibly** portable. i still find myself today reusing some of the components i wrote an entire year ago for a completely different project. additionally, roblox has so many great tools for roact in the pipeline that muscling through these early phases will really pay off in the future
16 Likes

In my experience (especially with using a library called Roact-Animate in order to achieve animation, which as it turns out, had a huge memory leak problem that was never fixed), the process of using refs is still overcomplicated and easy to mess up. I was unaware of “bindings” or this “otter” library though, and perhaps they do alleviate the issues with standalone Roact.

I guess this is an error on my part caused by misunderstanding Rodux.

I know that there’s a Virtual DOM and all that; that’s not what I’m talking about. Roact is an entirely declarative framework, meaning there is little room for anything other than pure declaration. Performance is only generally an issue when you have lots of reconciliation on complex objects in a frame; Rocrastinate limits this to one reconciliation per frame, and only reconciles the minimal number of properties which the developer has complete control over.

This issue in particular, performance issues aside, is why Roact is unusable for my team’s projects. Roact does not allow the use of UI templates; I have even made attempts automate the process of creating Roact components from templates, but it is so time-consuming, and hard for the non-scripter UI designers on my team to interface with, especially if they want to re-design or slightly update UI. And, moreover, once you start adding logic into this declaration, it becomes even harder to make changes between a composite design and the contents of the render() function.

Roblox has a great UI designing workflow. Roact gets in the way of that by forcing you to either script the things you render 100% of the time, or write an overly complex workflow to automate this, and still have to copy/paste code around to maintain the logic of a changed template. Roact is just not viable for teams unless everyone is a programmer and wants to design their all of their UI visuals by code.

I understand that this is not applicable to every scenario, but I think it’s still applicable in some inevitable scenarios in most games (e.g. an inventory/shop system that queries lots of items). Without creating/destroying instances, there’s still a lot of reconciliation that goes on that is loosely in the control of the developer.

1 Like

Also, I would say that these three reasons are pros for Roact, I don’t think they necessarily imply cons for Rocrastinate.

This is mostly LPGhatguy’s project, and Rocrastinate is mostly mine. Both libraries will evolve as time goes on, and I am certainly open to suggestions from other people as to what they want to see to improve the library.

I would say this applies more if you’re using roblox-ts, but I would definitely recommend using React/Redux in actual web applications; I don’t think Roact/Rodux is necessary for games. Rocrastinate projects can be structured in the same way, and I often times found myself looking at React tutorials to aid in the structure of my own Rocrastinate projects. I would say that this library is about as transferrable, and you still have to manage state and componentize your UI. That being said, it is not React, and neither is Roact React. If you want to actually develop these skills, you should make some React projects as well.
Here’s a tutorial series, which perhaps maybe applicable for Rocrastinate development in some ways. But ultimately I would say that no particular UI framework is mandatory for game development, and a user’s primary choice of using UI libraries should be based on the way in which they specifically make the process of game development easier.

As does Rocrastinate. In particular, I think Rocrastinate is more re-usable for roblox projects because it uses minimal logic to update templates, which can be modified to suit other projects; the logic all belongs in the Redraw function and can be slightly modified.

I also think that Decorator components (something somewhat unique to Rocrastinate) offers a lot in terms of re-usability and composition over inheritance. While it serves a similar function to Higher-Order Components, It can be utilized in ways that are a bit more difficult in to achieve in Roact.
image

3 Likes

This is my favorite thing right now, it’s great. I’m definitely going to use this a lot. It’s a good bit faster than Roact too (unsurprisingly).

2 Likes

Procrastinating. I’m far too good at that. The difference is that this library gets more done than me. :joy:

I’ve never actually tried out React or Redux but they have been two libraries high up on my list of things to experiment around with. I’ll definitely throw Rocrastinate there as well.

That being said, in a project, would it be recommended to use Rocrastinate only? I was thinking that perhaps all 3 libraries could coexist in a game’s codebase.

I would advise against using multiple component systems in a project because that path leads to spaghetti code and complications.

3 Likes

Im failing to see the comparisons between this and react though other than it has a lifecycle. The premise in Roact is that its a declarative way to design UI.

I found this in your repository though and I am a bit confused

local Rocrastinate = require(game.ReplicatedStorage.Rocrastinate)
local CoinsDisplay = Rocrastinate.Component:extend()

function CoinsDisplay:constructor(parent)
    self.coins = 0
    self.parent = parent
end

CoinsDisplay.RedrawBinding = "Heartbeat"
function CoinsDisplay:Redraw()
    if not self.gui then
        self.gui = self.maid:GiveTask(script.CoinsDisplayTemplate:Clone())
        self.gui.Parent = self.parent
    end
    
    self.gui.CoinsLabel.Text = "Coins: " .. self.coins
end

function CoinsDisplay:AddCoin()
    self.coins = self.coins + 1

    self.queueRedraw()
end

return CoinsDisplay

Your example files are in an rbxl and I’m not able or willing to download them right now. Can you elaborate how this is related to React?

Also I would suggest a library name that is shorter or at least easier to spell if you want people to use it.

This isn’t React, nor is it completely declarative (nor does it intend to be). However, I find it still lends itself to a similar project structure, and is more suitable for roblox in that way. I think the reason for this is because it has the same declarative flow of mapping state to visuals—except instead of immediately reconciling updates, it queues redraws and waits until the next (or current if it hasn’t rendered yet) RenderStep or Heartbeat depending on the redraw binding. It’s also less declarative because of the use of templates, and only requires the minimal amount of declaration—i.e. the parts of redrawing that’s based on logic. Declaring the static parts of a component is redundant, and is better left for designed UI templates.

I named it Rocrastinate sort of as an antonym to Roact (i.e. I don’t want the visual response to state to be reactive; I want it to wait until the last minute).

As for my example files, unfortunately they must be rbxl because Rocrastinate uses templates. It’s hard to sort that into Lua files, and declaring the UI templates would make it more verbose than it needs to be.

1 Like

This looks interesting, I have been considering carefully what path I should take with my GUI. I am looking at React/Rodux as well as this and I have not quite wrapped my head around either, in particular how to uses DataStores in conjunction with them.

How is Rocrastiante working alongside a cached Datastore? What if we are using Datastore2, which caches data then saves it at appropriates times, can it sync form the cached data in that module? I see you have to add data directly like this:

function CoinsDisplay:AddCoin()
    self.coins = self.coins + 1

    self.queueRedraw()
end

Then we dont have a single source of truth. Wouldn’t it be much better to simply sync values from an existing data cache than to manually pass them in each time? How does this solve the problem of having to pass data to the client every time it changes?

I have been working on an approach that uses ObjectValues who have a Changed event linked to various parts of the GUI, if the ObjectValue changes then it will instantly change the values on the the GUI. Then all I need is a module that keeps the ObjectValues synced to the players data.

Does Rocrastinate do this more elegantly? Sorry for noobish questions because, well, I am a noob :slight_smile:

Further in the tutorial, I showed two examples for the CoinsDisplay: one that uses AddCoin (i.e. local state) and one that uses Rocrastinate’s Redux-like features (i.e. a single source of truth).

Passing data from an external source can happen in multiple ways.

One way is through middlewares (which I should probably write more about soon); Rocrastinate comes packaged with a few of them (here is an example of a use case for “thunk middleware”, common on react/redux apps: https://stackoverflow.com/questions/39813984/how-to-fetch-data-through-api-in-redux); you can have components “fetch” data periodically from the server through endpoints if it’s something relatively static. If it’s something that changes a lot and needs to be responsive, you probably want to listen to the server and forward it to the store using store.dispatch.

If the store keeps track of data, but isn’t the originator for that data, it would help if your external data API had an easy way to observe and retrieve changes in that source, and forward it to Rocrastinate’s store.

Alternatively, you can skip stores entirely and instead “connect” your components to that external source of data through your own connectors; it’s best if the API to observe updates is maidable.

Something like this would be appropriate:

local MyComponent = Rocrastinate.Component:extend()

function MyComponent:constructor(...)
    self.someValue = (some initial value)

    self.maid:GiveTask(
        someAPI.someValueChanged:Connect(function(newValue)
            self.someValue = newValue
            self.queueRedraw()
        end)
    )
end

function MyComponent:Redraw()
    ( do something with self.someValue, or alternatively show "loading..." until it is retrieved. )
end

No store/actions/reducers/middleware are used here.

If you use RemoteEvents, the connections from OnClientEvent are maidable. Othewise, you might want to consider BindableEvents, or a module that makes observing changes maidable.

This looks interesting, especially since I never really got into using Roact. This appears to be a smart way to encapsulate all the hairy UI updating stuff into reusable components! I made my own UI framework before that basically did this same thing, but it didn’t get very far, so it’s nice to see someone else had the motivation to see it through to completion :slight_smile:

4 Likes

I just published fixes to Rocrastinate to make the library compatible with changes from this update:

If you’re using Rocrastinate (or my new library UILite UILite V1.04 - Roblox) in any of your code, please insert the latest version of the model (Rocrastinate - Roblox) or check the github for the latest version.