Graph
The reactive graph: signals hold values, computeds derive them lazily, effects react to them, and scopes own the reactions and cleanups created inside them. The documented members are flat on Flux.
local count = Flux.signal(0)
local doubled = Flux.computed(function()
return count() * 2
end)
Flux.effect(function() print(doubled()) end) -- prints 0, then 10 on the flush after `count(5)`
count(5)Types
Cleanable type
type Cleanable = (() -> ())
| ((oldValue: any) -> ())
| ((oldValue: any, destroyed: boolean) -> ())
| thread
| Instance
| RBXScriptConnection
| { read Destroy: (self: any) -> () }
| { read destroy: (self: any) -> () }
| { read Disconnect: (self: any) -> () }
| { read disconnect: (self: any) -> () }
| { read [number]: Cleanable }Context type
type Context<T> = {
read default: T,
--[[
Returns the value from the innermost active `provide` call, or `default` if none is active. Computeds, effects, and
scopes remember the context values active when they were created.
[Open Documentation](https://flux.kohl.gg/guide/concepts/context#providing-and-reading)
]]
get: (self: any) -> T,
--[[
Runs `fn` with this context set to `value` for all code and reactive nodes created inside `fn`, then restores the
previous value.
[Open Documentation](https://flux.kohl.gg/guide/concepts/context#providing-and-reading)
]]
provide: <R>(self: any, value: T, fn: () -> R) -> R,
}Node type
type Node<T = any> = setmetatable<{
--[[
Reads the node's current value without subscribing the caller.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
]]
read peek: (self: any) -> T,
--[[
Reads the node's current value (`node()` does the same) and subscribes the running
[computed](https://flux.kohl.gg/guide/concepts/computeds) or [effect](https://flux.kohl.gg/guide/concepts/effects).
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
]]
read get: (self: any) -> T,
--[[
Writes `value` (`node(value)` does the same), skipping propagation when it equals the current value (`~=` or the
node's `equals`). Pass `force = true` to notify dependents anyway.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#updating-a-signal)
]]
read set: (self: any, value: any, force: boolean?) -> T,
--[[
Notifies the node's dependents without changing its value, useful after mutating a table in place.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#forcing-an-update)
]]
read update: <N>(self: N) -> N,
--[[
Disconnects the node from its sources and observers, then runs its pending [`Flux.cleanup`](https://flux.kohl.gg/guide/concepts/effects#cleanup-with-flux-cleanup)
values and `onDestroy` callbacks. Later reads return `nil`.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
]]
read Destroy: (self: any) -> (),
--[[
Registers `fn` to run with the node's last value when the node is destroyed. Unlike `Flux.cleanup`, `onDestroy`
callbacks only run on final teardown and survive re-runs.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
]]
read onDestroy: (self: any, fn: (oldValue: T) -> ()) -> (),
}, Operators<T>>A reactive node holding a T (a signal, computed, effect, or scope). Covariant in T so Node<"a"> is assignable to Node<string>.
State type
type State<T = any> = setmetatable<{
--[[
Reads the node's current value without subscribing the caller.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
]]
read peek: (self: any) -> T,
--[[
Reads the node's current value (`node()` does the same) and subscribes the running
[computed](https://flux.kohl.gg/guide/concepts/computeds) or [effect](https://flux.kohl.gg/guide/concepts/effects).
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
]]
read get: (self: any) -> T,
--[[
Writes `value` (`node(value)` does the same), skipping propagation when it equals the current value (`~=` or the
node's `equals`). Pass `force = true` to notify dependents anyway.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#updating-a-signal)
]]
read set: (self: any, value: T, force: boolean?) -> T,
--[[
Notifies the node's dependents without changing its value, useful after mutating a table in place.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#forcing-an-update)
]]
read update: <N>(self: N) -> N,
--[[
Disconnects the node from its sources and observers, then runs its pending [`Flux.cleanup`](https://flux.kohl.gg/guide/concepts/effects#cleanup-with-flux-cleanup)
values and `onDestroy` callbacks. Later reads return `nil`.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
]]
read Destroy: (self: any) -> (),
--[[
Registers `fn` to run with the node's last value when the node is destroyed. Unlike `Flux.cleanup`, `onDestroy`
callbacks only run on final teardown and survive re-runs.
[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
]]
read onDestroy: (self: any, fn: (oldValue: T) -> ()) -> (),
}, Operators<T>>A writable signal node whose :set(value) method is type-checked against T. Assignable wherever Node<T> or Reactive<T> is expected.
Reactive type
type Reactive<T> = T | Node<T> | (() -> T)A plain T, a Node<T>, or a () -> T getter function. Read it with Flux.read or Flux.raw.
type Props = { label: Flux.Reactive<string>, disabled: Flux.Reactive<boolean>? }
local function Label(props: Props) end
local function label<A>(value: A): Flux.Node<Flux.Unwrap<A>>
return Flux.computed(function()
return Flux.read(value)
end)
endUnwrap type function
type function Unwrap(value: type)Extracts the underlying value type T from a Node<T>, getter () -> T, or plain T (distributing over unions).
Owner type
type Owner = { any }Functions
new
Graph.new(obj: any, isEffect: boolean?, equals: any): anyWraps obj in a reactive node: a signal for a plain value, a computed for a function, or an effect when isEffect is true (existing nodes pass through as-is).
signal
Graph.signal(initial: any, equals: any): anyCreates a writable signal holding initial, using equals (default ==) to skip unchanged writes. Signals aren't owned by enclosing scopes and are garbage-collected once unreferenced.
local count = Flux.signal(0)
count(count() + 1)computed
Graph.computed(fn: any, equals: any): anyCreates a lazily cached computed node derived from fn. Re-evaluates only when read after a dependency changes, and uses equals (default ==) to stop propagation when the result is unchanged.
local total = Flux.computed(function()
return price() * quantity()
end)effect
Graph.effect(fn: (...any) -> ...any): anySchedules fn to run on the next flush and re-run whenever any node it reads changes. Owned by the enclosing scope or stopped manually with node:Destroy().
Flux.effect(function() print("count is", count()) end)getter
Graph.getter(node: any): anyReturns a plain function that reads node with tracking, avoiding wrapper __call overhead in hot loops.
local count = Flux.signal(0)
local getCount = Flux.getter(count)
Flux.effect(function()
print(getCount())
end)setter
Graph.setter(node: any): anyReturns a plain function that writes node (setter(value, force)), avoiding wrapper __call overhead in hot loops.
createSignal
Graph.createSignal<T>(initial: T, equals: ((T, T: any) -> boolean)?): (() -> T, (value: T, force: boolean?) -> T)Creates a lightweight signal without a node wrapper and returns (getter, setter). Faster and smaller than Flux.signal, with no methods (Destroy, onObserved, etc.).
local count, setCount = Flux.createSignal(0)
Flux.effect(function()
print(count())
end)
setCount(count() + 1)createMemo
Graph.createMemo<T>(fn: () -> T, equals: ((T, T) -> boolean)?): () -> TCreates a lightweight computed without a node wrapper and returns its tracked getter function. Owned by the active scope or reaction like Flux.computed.
local count, setCount = Flux.createSignal(1)
local doubled = Flux.createMemo(function()
return count() * 2
end)createEffect
Graph.createEffect(fn: (...any) -> ...any)Creates a lightweight effect without returning a node wrapper. Owned by the active scope or reaction like Flux.effect.
local count, setCount = Flux.createSignal(0)
Flux.createEffect(function()
print("count is", count())
end)flush
Graph.flush()Runs all queued effects immediately instead of waiting for the next frame.
scope
Runs fn in a new owner scope and returns (scope, result). Calling scope:Destroy() tears down every computed, effect, instance, and cleanup created inside fn.
local scope, frame = Flux.scope(function() return Flux.new "Frame" { Parent = gui } end)
scope:Destroy() -- destroys the frame and everything bound to itcleanup
Graph.cleanup(obj: Cleanable)Registers obj to be disposed via Flux.dispose before the running reaction re-runs and when its owner is destroyed. Cleanup functions receive (previousValue, isDestroyed).
Flux.effect(function()
local connection = button.Activated:Connect(onClick)
Flux.cleanup(connection)
end)dispose
Graph.dispose(obj: Cleanable?)Tears down obj immediately (functions, instances, connections, threads, objects with Destroy/Disconnect methods, or arrays of cleanables). Runs untracked and warns on error instead of throwing.
local connections = { part.Touched:Connect(onTouched), part.Destroying:Connect(onGone) }
Flux.dispose(connections)getOwner
Graph.getOwner(): Owner?Returns the active reactive owner, or nil at the root. Pass it to Flux.withOwner to attach deferred work to the same scope.
withOwner
Graph.withOwner(node: any, fn: () -> ...any): ...anyRuns fn with owner (from Flux.getOwner, a node, or nil for unowned roots) as the active owner and restores its captured context. If owner is already destroyed, anything created inside fn is disposed as soon as fn finishes.
untrack
Graph.untrack(fn: (() -> ...any)?): ...anyRuns fn without dependency tracking and returns its result. Called with no arguments (Flux.untrack()), it suspends tracking for the rest of the current reaction body or until Flux.retrack().
Flux.effect(function()
local id = selected() -- tracked
local name = Flux.untrack(function() return names()[id] end) -- not tracked
end)retrack
Graph.retrack()Resumes tracking after a bare Flux.untrack() in the same function body.
Flux.untrack()
local snapshot = rows()
Flux.retrack()raw
Graph.raw(object: any): anyReads a Reactive value (node, getter function, or plain value) without subscribing the running reaction.
read
Graph.read(object: any): anyReads a Reactive value (node, getter function, or plain value) and subscribes the running reaction if it's dynamic.
local function Label(props: { text: Flux.Reactive<string> })
return Flux.new "TextLabel" { Text = function() return string.upper(Flux.read(props.text)) end }
endsafe
Graph.safe<T>(tryFn: () -> T, fallback: T | (err: unknown) -> T, equals: ((a: T, b: T) -> boolean)?): Node<T>Creates a computed that evaluates tryFn and returns fallback (a static value or an untracked (err) -> T function) if tryFn throws. Automatically recovers when tracked dependencies change.
context
Graph.context(default: any): anyCreates a context with the given default value for passing data down a component tree without prop drilling.
local Theme = Flux.context("dark")
Theme:provide("light", function() return App {} end) -- App and everything it builds reads "light"bindContext
Graph.bindContext<A..., R...>(fn: (A...) -> R...): (A...) -> R...Wraps fn so every call sees the context values active when bindContext was called. Event handlers in Flux.new/Flux.edit do this automatically.
local onClick = Flux.bindContext(function()
print(ThemeContext:get()) -- the value provided where onClick was made
end)isNode
Graph.isNode(object: any): booleanReturns true if object is a Flux node.
isReactive
Graph.isReactive(): booleanReturns true if called inside a tracking computed or effect.
Node
Constructors: Graph.effect · Graph.scope
peek
Node:peek(): anyReads the node's current value without subscribing the caller.
get
Node:get(): anyReads the node's current value (node() does the same) and subscribes the running computed or effect.
set
Node:set(value: any, force: boolean?): anyWrites value (node(value) does the same), skipping propagation when it equals the current value (~= or the node's equals). Pass force = true to notify dependents anyway.
update
Node:update(): anyNotifies the node's dependents without changing its value, useful after mutating a table in place.
Destroy
Node:Destroy()Disconnects the node from its sources and observers, then runs its pending Flux.cleanup values and onDestroy callbacks. Later reads return nil.
onDestroy
Node:onDestroy(fn: (oldValue: any) -> ())Registers fn to run with the node's last value when the node is destroyed. Unlike Flux.cleanup, onDestroy callbacks only run on final teardown and survive re-runs.
onObserved
Node:onObserved(fn: () -> ())Registers fn to run when the node gains its first transitive effect observer. Pair with onUnobserved to start and stop external listeners on demand.
onUnobserved
Node:onUnobserved(fn: () -> ())Registers fn to run when the node loses its last transitive effect observer.
Context
Constructors: Graph.context
get
Context:get<T>(): TReturns the value from the innermost active provide call, or default if none is active. Computeds, effects, and scopes remember the context values active when they were created.
provide
Context:provide<T, R>(value: T, fn: () -> R): RRuns fn with this context set to value for all code and reactive nodes created inside fn, then restores the previous value.