Skip to content

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.

luau
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 ​

luau
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 ​

luau
type Context<T> = {
	read default: T,
	--[[
	Reads the current value: the innermost `provide` around the running code, falling back to the `default` when
	none is active. A [computed](https://flux.kohl.gg/guide/concepts/computeds), [effect](https://flux.kohl.gg/guide/concepts/effects),
	or [scope](https://flux.kohl.gg/guide/concepts/scopes) remembers the values provided where it was <em>created</em>, so its
	later runs keep reading them.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/context#providing-and-reading)
	]]
	get: (self: any) -> T,
	--[[
	Runs `fn` with this context set to `value`, so any `get` reached during `fn` (no matter how deep) sees
	`value`, and so do the later runs of every computed, effect, and scope created during `fn`. The previous value is
	restored once `fn` returns or throws. Returns whatever `fn` returns.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/context#providing-and-reading)
	]]
	provide: <R>(self: any, value: T, fn: () -> R) -> R,
}

Node type ​

luau
type Node<T = any> = setmetatable<{
	--[[
	Reads the node's up-to-date value <strong>without</strong> 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. This is a <strong>tracked</strong> read: inside a
	[computed](https://flux.kohl.gg/guide/concepts/computeds) or [effect](https://flux.kohl.gg/guide/concepts/effects) it
	subscribes the running reaction, which re-runs when this node changes. `node()` is the same read; `node:peek()` reads
	without subscribing.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
	]]
	read get: (self: any) -> T,
	--[[
	Writes a value, skipping propagation when it equals the current one (`~=`, or the node's `equals`). Pass `force` as
	`true` to notify dependents anyway; `node(value)` is the same write. Returns `value`.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#updating-a-signal)
	]]
	read set: (self: any, value: any, force: boolean?) -> T,
	--[[
	Re-fires the node's dependents <strong>without writing</strong>, as if a changed value had been set: the way to push
	an in-place table mutation (same reference) through the graph. Returns the node.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#forcing-an-update)
	]]
	read update: <N>(self: N) -> N,
	--[[
	Destroys the node: severs every link to its sources and observers, then runs its pending `Flux.cleanup` values and
	`node:onDestroy` callbacks. A later read returns `nil`; `Flux.isNode` stays `true`. Destroying twice is a no-op.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
	]]
	read Destroy: (self: any) -> (),
	--[[
	Runs `fn` with the node's last value when the node is destroyed: by `node:Destroy()`, or with the scope that owns a
	computed or effect. A signal has no owner, so only its own `Destroy` fires it. Unlike `Flux.cleanup`, it survives
	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 cell holding a T: a signal, computed, effect or scope. Call it to read (node()) or write (node(v)). Covariant, so a Node<"a"> is a Node<string> and a Node<string> a Node<string?>: the type props and parameters take. A write through a Node<T> is not checked against T; the State<T> a signal returns is.

State type ​

luau
type State<T = any> = setmetatable<{
	--[[
	Reads the node's up-to-date value <strong>without</strong> 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. This is a <strong>tracked</strong> read: inside a
	[computed](https://flux.kohl.gg/guide/concepts/computeds) or [effect](https://flux.kohl.gg/guide/concepts/effects) it
	subscribes the running reaction, which re-runs when this node changes. `node()` is the same read; `node:peek()` reads
	without subscribing.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#reading-a-signal)
	]]
	read get: (self: any) -> T,
	--[[
	Writes a value, skipping propagation when it equals the current one (`~=`, or the node's `equals`). Pass `force` as
	`true` to notify dependents anyway; `node(value)` is the same write. Returns `value`.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#updating-a-signal)
	]]
	read set: (self: any, value: T, force: boolean?) -> T,
	--[[
	Re-fires the node's dependents <strong>without writing</strong>, as if a changed value had been set: the way to push
	an in-place table mutation (same reference) through the graph. Returns the node.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#forcing-an-update)
	]]
	read update: <N>(self: N) -> N,
	--[[
	Destroys the node: severs every link to its sources and observers, then runs its pending `Flux.cleanup` values and
	`node:onDestroy` callbacks. A later read returns `nil`; `Flux.isNode` stays `true`. Destroying twice is a no-op.
	[Open Documentation](https://flux.kohl.gg/guide/concepts/signals#destroying-a-signal)
	]]
	read Destroy: (self: any) -> (),
	--[[
	Runs `fn` with the node's last value when the node is destroyed: by `node:Destroy()`, or with the scope that owns a
	computed or effect. A signal has no owner, so only its own `Destroy` fires it. Unlike `Flux.cleanup`, it survives
	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 source, as Flux.signal returns it: a Node<T> whose set is checked, so state:set(5) on a string state is an error (the call form state(5) is not checked, so typed code writes with :set). Exact in T; pass it wherever a Node<T> or Reactive<T> is taken.

Reactive type ​

luau
type Reactive<T> = T | Node<T> | (() -> T)

A value that may be reactive: a plain T, a node whose value fits T, or a getter returning a T. Flux.read reads any of them, and Flux.new binds any of them to a property. A generic helper should take any A and return Unwrap<A>: a <T>(value: Reactive<T>) parameter binds T to the node itself when given a node.

luau
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)
end

Unwrap type function ​

luau
type function Unwrap(value: type)

The value a Reactive reads as: a node's value, a getter's result, or the value itself. Distributes over unions, so Unwrap<Reactive<string>?> is string?. The result type of Flux.read and Flux.raw.

Owner type ​

luau
type Owner = { any }

Functions ​

new ​

luau
Graph.new(obj: any, isEffect: boolean?, equals: any): any

Creates a node from obj: a signal for a value, a computed for a function, or an effect for a function when isEffect is true. A node is returned as is.

signal ​

luau
Graph.signal(initial: any, equals: any): any

Creates a writable signal holding initial. Reading it inside a reaction subscribes; writing a new value re-runs dependents. equals replaces the ~= change check. A signal is never owned by a scope: the GC reclaims it once dropped.

luau
local count = Flux.signal(0)
count(count() + 1)

Open Documentation

computed ​

luau
Graph.computed(fn: any, equals: any): any

Creates a lazily cached node derived from fn. It re-evaluates only when read after a dependency changed, and equals stops propagation when the new result compares equal.

luau
local total = Flux.computed(function()
	return price() * quantity()
end)

Open Documentation

effect ​

luau
Graph.effect(fn: (...any) -> ...any): any

Creates an effect: fn runs on the next flush and again whenever a node it read changes. Returns the effect node; node:Destroy() stops it, as does destroying the scope that created it.

luau
Flux.effect(function() print("count is", count()) end)

Open Documentation

getter ​

luau
Graph.getter(node: any): any

A plain function that reads node with tracking, like node(): for hot code, since calling it skips the wrapper's __call dispatch. It works anywhere a getter does (Flux.read, a binding, a prop).

luau
local count = Flux.signal(0)
local getCount = Flux.getter(count)
Flux.effect(function()
	print(getCount())
end)

setter ​

luau
Graph.setter(node: any): any

A plain function that writes node, like node:set(value, force) (so nil is a write): for hot code, since calling it skips the wrapper's __call dispatch.

createSignal ​

luau
Graph.createSignal<T>(initial: T, equals: ((T, T: any) -> boolean)?): (() -> T, (value: T, force: boolean?) -> T)

A signal without a node: returns a getter and a setter, like Solid's createSignal. The getter reads with tracking, like node(); the setter writes, like node:set(value, force) (so nil is a write). They are smaller and faster than a node from Flux.signal, and they work anywhere a getter does (Flux.read, a binding, a prop), but there are no methods: nothing to Destroy (the signal is collected once nothing holds the functions), no onObserved or Flux.dispose.

luau
local count, setCount = Flux.createSignal(0)
Flux.effect(function()
	print(count())
end)
setCount(count() + 1)

createMemo ​

luau
Graph.createMemo<T>(fn: () -> T, equals: ((T, T) -> boolean)?): () -> T

A computed without a node: returns its getter, like Solid's createMemo. It is owned like Flux.computed (destroyed with the scope or reaction that created it) and reads like node(), but it has no methods.

luau
local count, setCount = Flux.createSignal(1)
local doubled = Flux.createMemo(function()
	return count() * 2
end)

createEffect ​

luau
Graph.createEffect(fn: (...any) -> ...any)

An effect without a node: schedules fn on the next flush and re-runs it whenever a node it read changes, like Solid's createEffect. It is owned like Flux.effect (destroyed with the scope or reaction that created it), but returns nothing.

luau
local count, setCount = Flux.createSignal(0)
Flux.createEffect(function()
	print("count is", count())
end)

flush ​

luau
Graph.flush()

Runs the pending effects now instead of on the next frame. Mainly for tests, to observe a write's effects synchronously.

Open Documentation

scope ​

luau
Graph.scope<T>(fn: (scope: Node<any>) -> T): (Node<any>, T)

Runs fn in a new owner scope and returns (scope, result). The computeds, effects, instances and cleanup values created inside are torn down together by scope:Destroy().

luau
local scope, frame = Flux.scope(function() return Flux.new "Frame" { Parent = gui } end)
scope:Destroy() -- destroys the frame and everything bound to it

Open Documentation

cleanup ​

luau
Graph.cleanup(obj: Cleanable)

Registers obj to be disposed, like Flux.dispose, right before the running computed or effect re-runs and when its owner is destroyed, after the owner's children are torn down in both cases. A function receives the owner's previous value and a destroyed boolean (true on final teardown, false on re-computation). Errors outside an owner, and on a value Flux.dispose can't tear down.

luau
Flux.effect(function()
	local connection = button.Activated:Connect(onClick)
	Flux.cleanup(connection)
end)

Open Documentation

dispose ​

luau
Graph.dispose(obj: Cleanable?)

Disposes obj right now, the same teardown a scope runs on its Flux.cleanup values. A function is called with no arguments, an Instance is destroyed, a connection is disconnected, a suspended thread is canceled, a table is torn down by its Destroy/destroy/ Disconnect/disconnect method (a node, a scope, an async, a selector, a store) or else, when it is an array, each item in turn. It runs untracked, so reads inside a disposed function never subscribe the running reaction. An error is warned, never raised, and anything else (nil included) is ignored.

luau
local connections = { part.Touched:Connect(onTouched), part.Destroying:Connect(onGone) }
Flux.dispose(connections)

Open Documentation

getOwner ​

luau
Graph.getOwner(): Owner?

Returns the current owner, or nil at the root. Capture it to create reactions from deferred work that still belong to it, with Flux.withOwner.

Open Documentation

withOwner ​

luau
Graph.withOwner(node: any, fn: () -> ...any): ...any

Runs fn with owner (from Flux.getOwner, or a node) as the current owner, so the reactions and cleanups created inside belong to it, and the context provided where it was created is visible again. Tracking is left as it is. Pass nil to create unowned roots. On an owner already destroyed (a late callback), what fn creates and registers is disposed as soon as fn returns or throws.

Open Documentation

untrack ​

luau
Graph.untrack(fn: (() -> ...any)?): ...any

Runs fn without tracking: reads inside don't subscribe the running computed or effect. Returns what fn returns. Called with no function, it stops tracking for the rest of the current run, or until Flux.retrack().

luau
Flux.effect(function()
	local id = selected() -- tracked
	local name = Flux.untrack(function() return names()[id] end) -- not tracked
end)

Open Documentation

retrack ​

luau
Graph.retrack()

Resumes tracking after a bare Flux.untrack() in the same body, restoring exactly the reaction it suspended, so the pair fences a stretch of untracked reads. Without a matching bare untrack() (in a scope body, inside untrack(fn) or withOwner, or when tracking is already on) it changes nothing. Pair untrack() and retrack() within one function body: a getter read through Flux.raw does not get a slot of its own.

luau
Flux.untrack()
local snapshot = rows()
Flux.retrack()

Open Documentation

raw ​

luau
Graph.raw(object: any): any

Reads a Reactive without tracking: a node's current value, a getter's result (the getter's own reads untracked too), or a plain value as is.

Open Documentation

read ​

luau
Graph.read(object: any): any

Reads a Reactive with tracking: a node's value, a getter's result, or a plain value as is. The way to read a prop that may be any of them.

luau
local function Label(props: { text: Flux.Reactive<string> })
	return Flux.new "TextLabel" { Text = function() return string.upper(Flux.read(props.text)) end }
end

Open Documentation

safe ​

luau
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, if it throws, falls back to fallback, re-running and recovering automatically whenever its tracked dependencies change. fallback may be a static value or a recovery function receiving the caught error; a recovery function runs untracked so it cannot accidentally subscribe to extra dependencies, and if it throws in turn, its own error propagates unchanged. Pass equals to customize change detection (default ==).

Open Documentation

context ​

luau
Graph.context(default: any): any

Creates a context with the given default, read by any code built under a provide without threading it through arguments. Outside any active provide, reads return default. Pair with components to share theme, services, or the current user down a tree.

luau
local Theme = Flux.context("dark")
Theme:provide("light", function() return App {} end) -- App and everything it builds reads "light"

Open Documentation

bindContext ​

luau
Graph.bindContext<A..., R...>(fn: (A...) -> R...): (A...) -> R...

Wraps fn so every call runs with the context values provided where it was wrapped, as a node created there would. Handlers declared in Flux.new/Flux.edit props get this already; use it for a connection made by hand, a task callback or a callback handed to other code, so a handler that opens a dialog draws it in the trigger's theme. Ownership and tracking are left as they are.

luau
local onClick = Flux.bindContext(function()
	print(ThemeContext:get()) -- the value provided where onClick was made
end)

Open Documentation

isNode ​

luau
Graph.isNode(object: any): boolean

Whether object is a node.

Open Documentation

isReactive ​

luau
Graph.isReactive(): boolean

Whether the caller runs inside a tracking computed or effect, where reads subscribe it.

Node ​

Constructors: Graph.effect · Graph.scope

peek ​

luau
Node:peek(): any

Reads the node's up-to-date value without subscribing the caller.

Open Documentation

get ​

luau
Node:get(): any

Reads the node's current value. This is a tracked read: inside a computed or effect it subscribes the running reaction, which re-runs when this node changes. node() is the same read; node:peek() reads without subscribing.

Open Documentation

set ​

luau
Node:set(value: any, force: boolean?): any

Writes a value, skipping propagation when it equals the current one (~=, or the node's equals). Pass force as true to notify dependents anyway; node(value) is the same write. Returns value.

Open Documentation

update ​

luau
Node:update(): any

Re-fires the node's dependents without writing, as if a changed value had been set: the way to push an in-place table mutation (same reference) through the graph. Returns the node.

Open Documentation

Destroy ​

luau
Node:Destroy()

Destroys the node: severs every link to its sources and observers, then runs its pending Flux.cleanup values and node:onDestroy callbacks. A later read returns nil; Flux.isNode stays true. Destroying twice is a no-op.

Open Documentation

onDestroy ​

luau
Node:onDestroy(fn: (oldValue: any) -> ())

Runs fn with the node's last value when the node is destroyed: by node:Destroy(), or with the scope that owns a computed or effect. A signal has no owner, so only its own Destroy fires it. Unlike Flux.cleanup, it survives re-runs.

Open Documentation

onObserved ​

luau
Node:onObserved(fn: () -> ())

Runs fn when the node becomes observed: when its first observer arrives, directly or through computeds, so that some effect now depends on it. Pair it with node:onUnobserved to start and stop work (a connection, a poll) only while anything is listening. A computed counts as observed while an effect transitively reads it.

onUnobserved ​

luau
Node:onUnobserved(fn: () -> ())

Runs fn when the node no longer has any observers: no effect depends on it anymore, directly or through computeds.

Context ​

Constructors: Graph.context

get ​

luau
Context:get<T>(): T

Reads the current value: the innermost provide around the running code, falling back to the default when none is active. A computed, effect, or scope remembers the values provided where it was created, so its later runs keep reading them.

Open Documentation

provide ​

luau
Context:provide<T, R>(value: T, fn: () -> R): R

Runs fn with this context set to value, so any get reached during fn (no matter how deep) sees value, and so do the later runs of every computed, effect, and scope created during fn. The previous value is restored once fn returns or throws. Returns whatever fn returns.

Open Documentation