Skip to main content

Structuring Dex Components

The previous two sections cover the fundamentals of Virtual Instances and State, and how to use to write reactive Components with Dex.

This section will go over some conventions and best practices for structuring UI components with Dex.

Using Props in Dex

With Dex, you can define a UI Component that takes in as many parameters as needed:

local function Component(text: string, position: UDim2)
return Dex.New("TextLabel", {
Text = text,
Position = position,
-- . . .
})
end

However, as more and more parameters are added to a UI component, it becomes increasingly more confusing what each argument is responsible for, and what order they should be passed in. Because of this, the convention for Dex Components is to always pass a single table argument to components called "props":

local function Component(props)
-- Extract different named parameters from the props table.
local text = props.text
local position = props.position

return Dex.New("TextLabel", {
Text = text,
Position = position,
-- . . .
})
end

Props is a concept borrowed from React, and mirrors the way new Virtual Instances are created:

    local buttonText = Dex.State("Click Me!")
return Dex.New("TextButton", {
Activated = function()
buttonText:Set("Button was clicked!")
end,
Text = buttonText,
BorderSizePixel = 0,
BackgroundColor3 = Color3.fromHex("fff"),
Position = UDim2.fromScale(0.5, 0.5),
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.fromScale(0.5, 0.1),
TextScaled = true,
}

In this example, Dex.New takes in three different types of objects as "properties" which work together to make an interactive UI:

  • Static Values (e.g. number, UDim2, Vector2, and Color3), which do not change over time
  • Observable Values (e.g. buttonText), which can change over time
  • Callbacks (e.g. Activated = function() ... end), which connect to input events

Props can also mirror this structure. Let's add a props parameter to the Button component, allowing for Button components to be instantiated multiple times in the UI. To do this, let's first lay out some design requirements:

  • Each button should have a different position
  • Each button should have a different text value, which can change over time
  • Each button should do something different when clicked.

With these requirements in mind, let's write out the type for a props table:

local function Button(props: {
position: UDim2,
text: Dex.Observable<string>,
activated: () -> (),
})

Here, we defined the structure of the props table using a type annotation. Dex makes use of Luau's Static Type System, and it is recommended to give type annotations to the props table of Dex Components, with --!strict mode enabled where possible.

The type annotation in the example above defines the following values in props:

  • position: A Static UDim2 value, representing where to place the button.
  • text: An Observable string, representing the text to display with the button (which changes over time).
  • activated: A Callback function, which is called when the button is pressed.

We can now refactor the Button Component to utilize the three values we defined in props, as well as utilize a Cloned Template to simplify the code:

local function Button(props: {
position: UDim2,
text: Dex.Observable<string>,
activated: () -> (),
})
return Dex.Clone(game.ReplicatedStorage.UITemplates.Button, {
Activated = props.activated,
Text = props.text,
Position = props.position,
})
end

Finally, we can achieve the same result as our original example by passing in the right props:

local function Gui()
local buttonText = Dex.State("Click Me!")
local button = Button({
text = buttonText,
position = UDim2.fromScale(0.5, 0.5),
activated = function()
buttonText:Set("Thanks :3")
end,
})
return Dex.New("ScreenGui", {ResetOnSpawn = false}, {button})
end
root:Render(Gui())

In Dex, the best practice for writing components is that Components should take in a single Props table as a parameter, and return a single VirtualInstance depending on the value of these Props.

Re-Using Components

We just saw a way of using props to aid in the abstraction of a UI component. Doing this also makes it easy to re-use code for UI components that appear to the user in multiple instances!

Let's write a Dex Component that creates a button which reveals a secret message when clicked:

local function SpoilerButton(props: {
previewText: string,
secretText: string,
position: UDim2,
})
local secretIsShown = Dex.State(false)
return Dex.Clone(game.ReplicatedStorage.UITemplates.SpoilerButton, {
Activated = function()
if secretIsShown:Current() then
return
end
secretIsShown:Set(true)
task.wait(2)
secretIsShown:Set(false)
end,
Text = secretIsShown:Map(function(currentSecretIsShown)
if currentSecretIsShown then
return props.secretText
else
return props.previewText
end
end),
Position = props.position,
})
end

Here, the props parameter takes in three static values, then uses Observable Mapping to switch between showing the preview text and the secret text based on an internal boolean state.

We can now re-use the interactive SpoilerButton component multiple times in our UI at once:

local function OpinionBio()
return Dex.New("ScreenGui", {ResetOnSpawn = false}, {
Button1 = SpoilerButton({
previewText = "Cats or Dogs?",
secretText = "Dogs",
position = UDim2.fromScale(0.5, 0.39),
}),
Button2 = SpoilerButton({
previewText = "Flavor of Ice Cream?",
secretText = "Strawberry",
position = UDim2.fromScale(0.5, 0.5),
}),
Button3 = SpoilerButton({
previewText = "Favorite Musician?",
secretText = "Erykah Badu",
position = UDim2.fromScale(0.5, 0.61),
}),
})
end

Since SpoilerButton uses a Premade Template, we can also adjust things like font, color, and padding in the UI without changing any of the code itself:

Optionally Observable Props

Props can define Static values or Observable values depending on the needs of a Component. However, there may be cases where you want to define a value that can be either a Static value or an Observable value

Dex provides a utility type CanBeObservable, which allows for something to be a static value or an Observable value in props. For any value type T, CanBeObservable<T> is just shorthand for the union type T | Observable<T> (i.e. "A value of type T or of type Observable<T>")

In the SpoilerButton Component, we can use the CanBeObservable type to allow both a Static string and an Observable string to be defined in props for previewText and secretText:

local function SpoilerButton(props: {
previewText: Dex.CanBeObservable<string>,
secretText: Dex.CanBeObservable<string>,
position: UDim2,
})

Now we can create a spoiler button with a Static string for previewText, and an Observable string for secretText, which changes every 4 seconds:

local secret = Dex.State(tostring(math.random(1, 1000)))
task.spawn(function()
while task.wait(4) do
secret:Set(tostring(math.random(1, 1000)))
end
end)

local button = SpoilerButton({
previewText = "Reveal Secret Number",
secretText = secret,
position = UDim2.fromScale(0.5, 0.5)
})

In order to parse this in a Component, we will need to use a helper function provided by Dex: CoerceAsObservable. This function takes in an object that can be an observable (CanBeObservable<T>), and returns an observable object (Observable<T>) of that same type.

Let's implement this in the SpoilerButton component:

local function SpoilerButton(props: {
previewText: Dex.CanBeObservable<string>,
secretText: Dex.CanBeObservable<string>,
position: UDim2,
})
-- Convert optionally observable props to Observable<string> objects
local previewText = Dex.CoerceAsObservable(props.previewText)
local secretText = Dex.CoerceAsObservable(props.secretText)

-- Derive the final text output from all observable objects' current values
local textOutput = Dex.Map(secretIsShown, previewText, secretText)(function(
currentSecretIsShown,
currentPreviewText,
currentSecretText
)
if currentSecretIsShown then
return currentSecretText
else
return currentPreviewText
end
end)

local secretIsShown = Dex.State(false)
return Dex.Clone(game.ReplicatedStorage.UITemplates.SpoilerButton, {
Activated = function()
if secretIsShown:Current() then
return
end
secretIsShown:Set(true)
task.wait(2)
secretIsShown:Set(false)
end,
Text = textOutput,
Position = props.position,
})
end

The SpoilerButton Component will now work the same as it did before in the OpinionBio example, where secretText is a static string value, but will also now work in cases where secretText is an Observable string:


The conventions outlined in this section are helpful for writing reactive and re-usable Dex Components.

The next section will cover one final aspect concept needed to scale up a Dex user interface: dynamically Creating & Destroying UI Components based on state.