Pract - A Declarative UI Library That's Just Practical!

What is Pract?

Github: https://github.com/ambers-careware/pract
Documentation: Getting Started - Pract
Releases: Download the rbxm or zip file here

Pract is a declarative UI library, similar to existing libraries like LPGHatGuy’s Roact and Elttob’s Fusion, for building large-scale UI projects.

NOTE: Pract requires an intermediate understanding of Luau, and is compatible with both Rojo and Studio-only projects. Pract is best used in Luau’s --!strict mode.

Why Use Pract Over Other UI Libraries?

Unlike Roact/Fusion, Pract allows you to use pre-existing templates which will be cloned or decorated by your Pract code, rather than having to specify every single property of your UI objects in code. This means your code will look a lot more concise and flexible, and you can design your UI visuals through Roblox’s UI editor!

Pract is also compatible with Luau’s strict mode, and should not emit any script analysis warnings in your project (unlike Roact/Fusion).

Example Pract Code

With Pract, you can design your entire UI tree in code:

--!strict
local Pract = require(game.ReplicatedStorage.Pract)

local PlayerGui = game.Players.LocalPlayer:WaitForChild('PlayerGui')

-- Create our virtual GUI elements
local element = Pract.create('ScreenGui', {ResetOnSpawn = false}, {
    HelloLabel = Pract.create('TextLabel', {
        Text = 'Hello, Pract!',
        TextSize = 24,
        BackgroundTransparency = 1,
        Position = UDim2.fromScale(0.5, 0.35),
        AnchorPoint = Vector2.new(0.5, 0.5)
    })
})

-- Mount our virtual GUI elements into real instances,
-- parented to PlayerGui
local virtualTree = Pract.mount(element, PlayerGui)

Alternatively, you can use a template instead, and write more concise and flexible code:

--!strict
local Pract = require(game.ReplicatedStorage.Pract)

local PlayerGui = game.Players.LocalPlayer:WaitForChild('PlayerGui')

-- Create our virtual GUI elements from a cloned template
-- under this script
local element = Pract.stamp(script.MyGuiTemplate, {}, {
    HelloLabel = Pract.decorate({Text = 'Hello, Pract!'})
})

-- Mount our virtual GUI elements into real instances,
-- parented to PlayerGui
local virtualTree = Pract.mount(element, PlayerGui)

Both examples can generate the same instances:

Installation

You can install Pract using one of the following methods:

Method 1: Inserting Pract directly into your place

  1. Download the latest rbxm release on Github
  2. Right click the object in the Roblox Studio Explorer that you want to insert Pract into (such as ReplicatedStorage) and select Insert from File...
  3. Locate the rbxm file that you downloaded and click Open

Method 2: Syncing via Rojo

  1. Install Rojo and initialize your game as a Rojo project if you have not already done so
  2. Download the latest Source Code release (zip file) on Github
  3. Extract the Pract folder from the repository into a location of your choosing within your Rojo project’s source folder (e.g. src/shared)
  4. Sync your project using Rojo

Documentation

I have written a full documentation for Pract on the GitHub. The examples and API are subject to change in the future.

Contributing

While I have written the initial release of Pract on my own, Pract is a public domain and open source project. If you like the library and would like to contribute to the addition of new features, bugfixing, unit testing, or documentation, please let me know!

If anyone wants to help in the creation of a community Discord server for the Pract library, send me a DM on the devforum! This will depend on how well-received and widely-used the Pract library becomes after this first release post.

32 Likes

Just to verify, you can use VS Code to sync the files right?

You can using Rojo; there’s also a VS Code extension for Rojo that makes it easy to do so. You should read the Rojo guide to get that set up first, then follow the instructions for using Pract with Rojo.

1 Like

Yes, thats what I meant. Can use VS Code with the Rojo extension to sync the files? Or is it something different?

What is the difference between this and just using Roact and Fusion components?

Typically with Roact/Fusion, you have to write out every single property on every single object in your UI.

This can end up with really long code:

Example using Roact-like/Fusion-like elements:
local myLuckyNumber = math.random(1, 99)

local element = Pract.create('ScreenGui', {ResetOnSpawn = false}, {
    HelloLabel = Pract.create('TextLabel', {
        TextSize = 30,
        TextColor3 = Color3.fromRGB(0, 0, 0),
        Font = Enum.Font.GothamBold,
        Position = UDim2.fromScale(0.5, 0.5),
        AnchorPoint = Vector2.new(0.5, 0.5),
        Size = UDim2.fromOffset(100, 100),
        BackgroundTransparency = 1,
        Text = string.format('Your lucky number is %d', myLuckyNumber)})
    })
})

The more complex your UI design is, the worse this code will look.
With Pract, you can simply design this template in Roblox Studio, and then write a much concise code that looks and functions exactly the same:

local myLuckyNumber = math.random(1, 99)

local element = Pract.stamp(script.HelloPractTemplate, {}, {
    HelloLabel = Pract.decorate({
        Text = string.format('Your lucky number is %d', myLuckyNumber)
    })
})

People typically use tools like Roactify or Hydrogen to convert their UI to a Roact/Fusion component, but you have to re-write the functional parts of your code every time you want to run the converter again.

Pract lets you design your code whichever way you want; if you use templates, it will be much faster both to write your UI code and to design your UI; It also becomes possible to edit your UI’s design without actually changing the code.

Both methods will end up looking like this when you play the game:

4 Likes

Thank you for the detailed response. So if I understand correctly, templates refer to UI made
in Studio which is filled out through Pract afterwards, and can be placed anywhere. OK. That was my confusion.

Regarding:

I find that I actually write properties less with Roact than with Studio. Roactify/etc can’t make clean code because it does not understand where different components can come into play. Code is cleaner when components are written manually, and reused accordingly. This can be developed quickly through an instant previewer like Hoarcekat.

I’ll write an example JSX syntax (which is just a more concise way of doing createElement).

Here's what I mean In a typical codebase, with the most common components already built, an inventory menu would look like this:
// Inventory component
<OutlinedBackground native={{ Size: UDim2.fromScale(0.5, 0.5) }}>
	<MenuHeader text="Inventory" />
	<ExitButton
		callback={() => {
			// close the menu
		}}
	/>
	<OutlinedScrollContainer native={{ Size: UDim2.fromScale(0.5, 0.5) }}>
		<uigridlayout CellSize={UDim2.fromScale(0.3, 0.3)} />
		{...props.items.map(item => <ItemSquare itemData={item} />)}
	</OutlinedScrollContainer>
</OutlinedBackground>

Notice that I don’t have to write lots of properties manually. This is what something like Roactify would output, and why I don’t recommend using generators like this as the go-to way of creating the UI:

<imagelabel
	Size={UDim2.fromScale(0.5, 0.5)}
	BackgroundTransparency={0.5}
	BorderSizePixel={0}
	// etc
>
	<textlabel
		Size={UDim2.fromScale(0.8, 0.2)}
		Text="Inventory"
		TextScaled={true}
		BackgroundTransparency={1}
		BorderSizePixel={0}
		AnchorPoint={new Vector2(0.5, 0)}
		Position={UDim2.fromScale(0.5, 0)}
	/>
	<uipadding
		PaddingLeft={new UDim(0, 5)}
		PaddingTop={new UDim(0, 5)}
		PaddingBottom={new UDim(0, 5)}
		PaddingRight={new UDim(0, 5)}
	/>
	<textbutton
		Text="X"
		BorderSizePixel={0}
		BackgroundTransparency={1}
		AnchorPoint={new Vector2(1, 0)}
		Position={UDim2.fromScale(1, 0)}
		Size={UDim2.fromScale(0.1, 0.1)}
	>
		<uiaspectratioconstraint />
	</textbutton>
	{/*and so much more*/}
</imagelabel>

Of course this results in messy code.

So while yes, Roact can be used to create UIs with hundreds of lines of code, it can also be used to create the same thing with the code size being tens of times smaller, and you having to type less properties in code, and in Studio.

I just released the v0.9.7 version of Pract!

Heads up to anyone who has used the v0.9.6 release of Pract—make sure you update to the most recent version! The previous version has some bugs that I detected with Pract.deferredState and Pract.classComponent respectively.

Unit testing is still in the works, so bugs like these should be easier to iron out in the future with future releases. For now I am focusing on my own game’s development, and this is how I found and patched some bugs with Pract.

1 Like

Would it practically (xd) be all that different from taking the conversion code from Hydrogen, turning it into a ModuleScript, and using that ModuleScript to convert your UI into Fusion components?

Yes, because your Pract code only needs to encapsulate the relevant parts of your UI that changes.

A good example would be a stateful button component:

--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pract = require(ReplicatedStorage.Pract)

local TEMPLATE = ReplicatedStorage.TemplatesFolder.ButtonComponent

export type Props = {
    onClick: () -> (),
}
local ButtonComponent = Pract.withState(getHovering, setHovering)
    return function(props)
        return Pract.stamp(TEMPLATE, {
            MouseEnter = function()
                setHovering(true)
            end,
            MouseLeave = function()
                setHovering(false)
            end,
            MouseButton1Click = props.onClick,
        }, {
            HoverGlow = Pract.decorate({
                Visible = getHovering(),
            }),
        )
    end
end

-- If this component were a module, we would return here...
return ButtonComponent :: Pract.ComponentTyped<Props>

If you are familiar with the Pract docs, it will be super easy to read this code, figure out what it is doing, and make changes to the functional parts of the UI visuals. If you want to change the button’s color, size, add a UICorner, background color, alignment/position, size, etc. you can simply just change the template visually through roblox’s UI editor, instead of having to tough the code. The only things you have to change in code is if you rename functional elements such as the HoverGlow.

Compare that to this code, which looks and functions the exact same at runtime:

ButtonComponent without a template
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pract = require(ReplicatedStorage.Pract)

local TEMPLATE = ReplicatedStorage.TemplatesFolder.ButtonComponent

export type Props = {
    onClick: () -> (),
}
local ButtonComponent = Pract.withState(getHovering, setHovering)
    return function(props)
        return Pract.create("Frame", {
            BackgroundTransparency = 0.5,
            BackgroundColor3 = Color3.fromRGB(200, 200, 200),
            BorderColor3 = Color3.fromRGB(0, 0, 0),
            Position = UDim2.fromScale(0.5, 0.75),
            AnchorPoint = Vector2.new(0.5, 0.5),
            Size = UDim2.fromOffset(200, 50),
            ZIndex = 2,
            MouseEnter = function()
                setHovering(true)
            end,
            MouseLeave = function()
                setHovering(false)
            end,
            MouseButton1Click = props.onClick,
        }, {
            HoverGlow = Pract.create("ImageLabel", {
                Visible = getHovering(),
                Image = "rbxassetid://[some glow image ID]",
                ZIndex = 1,
                BackgroundTransparency = 1,
                Size = UDim2.fromScale(1.5, 1.5),
                AnchorPoint = Vector2.new(0.5, 0.5),
                Position = UDim2.fromScale(0.5, 0.5),
            }),
            UICorner = Pract.create("UICorner", {
                CornerRadius = UDim.new(0.15, 0),
            }
        )
    end
end

-- If this component were a module, we would return here...
return ButtonComponent :: Pract.ComponentTyped<Props>

The worst part is that, if you want to make changes, you can’t visualize it until you run a playtest.
In addition to all of this, roblox is soon planning to make a lot of great changes to their UI editor in the future. You can’t use Fusion/Roact with templates without using something like Hydrogen.

1 Like

It’s worth mentioning that Fusion will be adding instance hydration support with v0.2, which should completely solve this shortcoming.

The impact of this is also somewhat exaggerated as it ignores the presence of things like components, which encapsulates this stuff at the lowest level and leads to far cleaner code throughout the rest of your codebase. While it’s possible to implement components with hydration, from my own research UI-from-code solutions still offer the greatest level of control, cleanliness and modularity, as well as serving as a proper single source of truth for what your UI should look like.

Fair point - though this is not strictly true. Especially for projects which are fully managed via Rojo, it’s much more common to see dedicated UI developers which build UIs from scratch to match a design spec, rather than this kind of importing. It’s fair to acknowledge that there is certainly an audience for the importing workflow (after all, if there wasn’t we wouldn’t be working on it), but it’s not really typical usage.

8 Likes