Home › Concepts
Components
In Flux, components are plain Luau functions that run once to construct Roblox Instances and wire up reactive bindings.
type ButtonProps = {
text: Flux.Reactive<string>,
onClick: (() -> ())?,
}
local function PrimaryButton(props: ButtonProps): TextButton
return Flux.new "TextButton" {
Size = UDim2.fromOffset(200, 50),
BackgroundColor3 = Color3.fromRGB(0, 120, 255),
Text = props.text,
Activated = props.onClick,
}
endWhen state changes, PrimaryButton itself doesn't re-run. Only the bound Text property updates.
Consuming Components
Call component functions directly inside a Flux.new hierarchy:
local count = Flux.signal(0)
local screen = Flux.new "Frame" {
Size = UDim2.fromScale(1, 1),
PrimaryButton({
text = function()
return `Count: {count()}`
end,
onClick = function()
count(count() + 1)
end,
}),
}Handling Props (Static vs. Reactive)
Callers may pass a static value ("Submit"), a reactive Node, or a getter function (() -> string). Annotate flexible component props with Flux.Reactive<T> (which accepts T | Node<T> | () -> T).
1. Passing Props Directly to Instance Properties
Flux.new and Flux.edit accept static values, nodes, and getters natively, so you can pass a Flux.Reactive<T> prop straight to an Instance property:
local function StatusLabel(props: { status: Flux.Reactive<string> })
return Flux.new "TextLabel" {
Text = props.status, -- static strings assign once, nodes and getters bind reactively
}
end2. Reading Node-or-Value Props with Flux.read
When you need to transform a Flux.Reactive<T> prop inside a computation, call Flux.read(value). It reads a node or getter with tracking, or returns a plain value as-is:
local function StatusLabel(props: { status: Flux.Reactive<string> })
return Flux.new "TextLabel" {
Text = function()
return `Status: {string.upper(Flux.read(props.status))}`
end,
}
endUse Flux.isNode(value) when you explicitly need to check whether a value is a Flux node.
Splitting Props with Flux.split
Reusable components often accept both component-specific props (like label or variant) and pass-through Instance properties (like Size, Position, LayoutOrder, or children) in a single table.
Combine Flux.Props<ClassName, Extra> with Flux.split(props, keys) to separate your component's own keys from the rest and forward the remaining table to Flux.new:
type BadgeProps = Flux.Props<"TextLabel", {
label: Flux.Reactive<string>,
accent: Flux.Reactive<Color3>?,
}>
local OWN: { "label" | "accent" } = { "label", "accent" }
local function Badge(props: BadgeProps): TextLabel
local own, rest = Flux.split(props, OWN)
return Flux.edit(Flux.new "TextLabel" {
Text = function()
return `[ {Flux.read(own.label)} ]`
end,
TextColor3 = Flux.fallback(own.accent, Color3.fromRGB(120, 170, 255)),
})(rest)
end- Typed
PickandOmit: With a single typed key group (either an annotated literal array likelocal OWN: { "label" | "accent" }or a schema table),ownis typed asFlux.Pick<Props, Keys>andrestasFlux.Omit<Props, Keys>, keeping required props required. - Multiple groups: You can pass multiple key groups (
local a, b, rest = Flux.split(props, KEYS_A, KEYS_B)). Each group receives its matching keys, and the final return holds everything else (including numeric children and__ATTR/__EVENT/__TAG/__CLEANdirectives). - Non-mutating:
propsitself is never mutated.
Accepting Children
Arrays placed at a numeric index inside Flux.new or Flux.edit are flattened into children automatically, preserving sequential LayoutOrder:
local function Panel(props: { title: Flux.Reactive<string>, children: Flux.Children? })
return Flux.new "Frame" {
Size = UDim2.fromOffset(400, 300),
Flux.list({ gap = 8 }),
Flux.new "TextLabel" { Text = props.title },
props.children, -- flattened in place
}
endComponent Scopes and Lifecycles
When a component is called inside a parent Flux.scope, everything it creates attaches to that parent scope automatically.
When a standalone component creates internal Effects or resources whose lifetime should follow its own root Instance, wrap its body in Flux.scope and bind the scope to __CLEAN:
local function TimerLabel(): TextLabel
local _, label = Flux.scope(function(scope)
local elapsed = Flux.signal(0)
Flux.cleanup(Flux.Clock.connect("Heartbeat", function(dt)
elapsed(elapsed() + dt)
end))
return Flux.new "TextLabel" {
Text = function()
return string.format("%.1fs", elapsed())
end,
__CLEAN = { scope },
}
end)
return label
end