Pract.combine
A unique feature of Pract is that the library allows and encouraging mounting multiple elements to the same host! This allows you to have multiple components which modify or connect events to the same instance.
Pract.combine
will mount multiple elements to the same hostContext. One direct use case is having re-usable input-handling components
local function HoverInput(props: {began: () -> (), ended: () -> ()})
return Pract.decorate {
InputBegan = function(rbx: TextButton, input: InputObject)
if input.UserInputType == Enum.UserInputType.MouseMovement then
props.began()
end
end,
InputEnded = function(rbx: TextButton, input: InputObject)
if input.UserInputType == Enum.UserInputType.MouseMovement then
props.ended()
end
end,
-- Though this currently only captures desktop hovering, we could potentially use this to
-- capture mobile events (such as tapping over a GuiObject for a long period of time)
-- or any other cross-platform event handling!
}
end
local MyFancyButton = Pract.withDeferredState(function(getHovering, setHovering)
return function(props: {text: string, clicked: () -> ()})
return Pract.combine(
-- This component is purely visual; it simply defines the visuals from state and props!
Pract.stamp(script.FancyButtonTemplate, {
Text = props.text,
TextColor3 = if getHovering()
then Color3.fromRGB(0, 255, 255)
else Color3.fromRGB(255, 255, 255),
MouseButton1Click = props.clicked,
}),
-- This component is purely functional; it encapsulates a reusable hover input event
-- system that might otherwise be tedious to re-write every time we create a button in
-- our UI.
-- This component will decorate the instance created by `Pract.stamp` above.
Pract.create(HoverInput, {
began = function()
setHovering(true)
end,
ended = function()
setHovering(false)
end,
})
)
end
end)
Pract.combine
can make it really easy to make your UI cross-platform compatible, without having to re-write cross platform input code every time you create a new button! Simply create a decorator component like our HoverInput
above, and combine it with a platform-agnostic visual component!
Host Propogation
The order in which you combine elements matters when determining the host context of each element being combined.
For example, you can combine the element Pract.create("Frame")
with Pract.decorate({Size = UDim2.fromOffset(20, 20)})
to both create and decorate the same instance. As in the Hoverinput example, this can be used to make “decorative” components that add functionality to another pre-created element. The Pract reconciler will automatically match decorative elements with instancing element’s hosts. This means that the order in which you combine elements matters.
As a rule of thumb:
-
Place
Pract.stamp
andPract.create("ClassName")
elements earlier in the combined tuple. -
Place
Pract.decorate
andPract.index
elements later in the combined tuple. -
If a component returns a
Pract.stamp
/Pract.create("ClassName")
element, place thePract.create(Component)
expression earlier in the combined tuple. Otherwise, place it later.
A Caveat On Combine Order
When a Pract.combine
element is updated, the order of combined elements matter when determining which elements are unmounted/remounted! Here’s a simple example that highlights the behavior:
local function ValueDecorator(props: {value: string})
return Pract.decorate({
Value = props.value,
[Pract.OnMountWithHost] = function()
print(props.value .. " mounted!")
end,
[Pract.OnUnmountWithHost] = function()
print(props.value .. " unmounted!")
end,
})
end
local tree = Pract.mount(
Pract.combine(
Pract.create("StringValue"),
Pract.create(ValueDecorator, {value = "Foo"}),
Pract.create(ValueDecorator, {value = "Bar"})
),
workspace,
"MyStringValue"
)
This should create a StringValue named “MyStringValue” in workspace, with a value finally set to “Bar”. The output should show that “Foo” mounted and then “Bar” mounted:
Foo mounted!
Bar mounted!
If we update our tree with our “Foo” element removed, we will see the positionally-dependent behavior of combine
in action:
Pract.update(
tree,
Pract.combine(
Pract.create("StringValue"),
Pract.create(ValueDecorator, {value = "Bar"})
)
)
When our update finishes, we will still see a StringValue in workspace with a value of “Bar”… however, the output will show something interesting:
Bar unmounted!
What happened when we first mounted the tree is as follows:
- The
Pract.create("StringValue")
element is mounted, creating a StringValue - Our first
ValueDecorator
component is created with the props{value = "Foo"}
2b.OnMountWitHost
is called with the props{value = "Foo"}
- Our second
ValueDecorator
component is created with the props{value = "Bar"}
2b.OnMountWithHost
is called with the props{value = "Bar"}
When we updated our tree, Pract simply consolidated the ValueDecorator components:
- The
Pract.create("StringValue")
element is updated - Our first
ValueDecorator
component is updated with the props{value = "Bar"}
since we simply shifted our “Bar” component back one position in the tuple. This silently happens - Our second
ValueDecorator
component (which was mounted with the props{value = "Bar"}
) is unmounted, since there is no longer any element in that position. 3b.OnUnmountWithHost
is called with the props{value = "Bar"}
Pract will try to simplify and re-use an already-mounted component where possible. Keep this in mind when writing a Pract.combine
expression.
Caveat On Combining Conditional Elements
If your combine expression contains a conditional list of elements, make sure that all of your conditional elements (i.e. elements that are only sometimes in a component’s combine tuple) are placed last in the combine tuple where possible.
Example:
--!strict
local Pract = require(game.ReplicatedStorage.Pract)
local StampingComponent = require(game.ReplicatedStorage.StampingComponent)
local RecoloringComponent = require(game.ReplicatedStorage.RecoloringComponent)
local RepositioningComponent = require(game.ReplicatedStorage.RepositioningComponent)
local function MyComponent(props: {reposition: boolean})
local elements: {Pract.Element} = {}
table.insert(
elements,
Pract.create(StampingComponent, {})
)
table.insert(
elements,
Pract.create(RecoloringComponent, {})
)
-- Because this element is conditional, we want it to be placed LAST in our array of elements!
if props.reposition then
table.insert(
elements,
Pract.create(RepositioningComponent, {})
)
end
-- Convert our array of elements to a tuple of combined elements
return Pract.combine(unpack(elements))
end