API reference
Every public function exported from shARKlib. If you are new, read the guide first.
Bootstrap
SetTemplate(map) · SetTemplate(name, map)
SetTemplate(map: { [class]: { [prop]: any } }) → ()
Register property defaults for instances. The first form sets the unnamed default template. The second registers a named template you can later select via create(class, name). Multiple calls merge. Later writes override earlier ones for the same key.
sh.SetTemplate({ Frame = { BorderSizePixel = 0 } })
sh.SetTemplate("danger", {
TextButton = { BackgroundColor3 = Color3.fromRGB(220, 70, 70) },
})
Init().Init()
Init() → ()
Finalize templates and unlock the rest of the API. Pre-builds clone sources for every registered (class, name) pair so subsequent create() calls only pay for the clone plus user-supplied props.
Reactive core
source(initial, equals?)
source<T>(initial: T, equals?: (T, T) → boolean) → (T?) → T
Create a reactive cell. The returned function reads when called with no arguments and writes when called with one. Writes that compare equal (via equals, default ==) are skipped. Pass an updater function to compute from the previous value.
local count = sh.source(0)
print(count()) -- 0
count(5) -- write 5
count(function(n) return n + 1 end) -- updater
derive(fn, equals?)
derive<T>(fn: () → T, equals?: (T, T) → boolean) → () → T
A memoized derived value. Re-runs when any tracked dependency changes; only notifies its own observers when the result differs.
local doubled = sh.derive(function() return count() * 2 end)
effect(fn)
effect(fn: () → ()) → ()
Schedule fn to run immediately and again whenever any source it reads changes. Cleanup is bound to the enclosing scope.
sh.effect(function()
print("count is", count())
end)
watch(source, fn)
watch<T>(source: () → T, fn: (new: T, old: T?) → ()) → () → ()
Like effect, but skips the initial synchronous invocation and provides the previous value. Returns a disposer.
local stop = sh.watch(score, function(new, old)
print("score went from", old, "to", new)
end)
root(fn)
root<R...>(fn: (destroy: () → ()) → R...) → R...
Create a top-level reactive scope detached from any tracking parent. fn receives a destroy callback that disposes everything created inside it.
local destroy = sh.root(function(dispose)
sh.effect(function() ... end)
return dispose
end)
cleanup(fn)
cleanup(fn: () → ()) → ()
Register a callback that runs when the enclosing scope is re-evaluated or disposed.
untrack(fn)
untrack<T>(fn: () → T) → T
Run fn without subscribing the active scope to any source it reads.
batch(fn)
batch<T>(fn: () → T) → T
Defer queue flushing until the outermost batch returns. Multiple writes inside fn coalesce into a single re-evaluation pass.
context(default)
context<T>(default: T) → Context<T>
Lightweight dependency injection scoped to the ownership tree.
local Theme = sh.context({ accent = Color3.new(1,0,0) })
-- somewhere up the tree
Theme:provide({ accent = Color3.new(0,1,0) }, function()
return App()
end)
-- somewhere down the tree
local theme = Theme:read()
read(value)
read<T>(value: T | () → T) → T
Resolve a value that may be either a plain value or a 0-arg getter. Useful for accepting either as a prop.
errorBoundary(try, fallback)
errorBoundary(try: () → any, fallback: (err, reset: () → ()) → any) → () → any
Catch errors thrown anywhere in the descendant graph and render fallback instead. Calling reset() re-runs try.
sh.errorBoundary(App, function(err, reset)
return sh.create("TextButton") {
Text = "error: " .. tostring(err) .. " (click to retry)",
Activated = reset,
}
end)
lazy(loader)
lazy<T>(loader: () → () → T) → () → T
Defer construction of a component until first render. loader is called once; subsequent reads use the cached component.
signal()
signal<T...>() → Signal<T...>
An allocation-friendly pub/sub primitive. Returns an object with connect, once, fire, wait, and destroy. Connections auto-disconnect on scope cleanup.
local toast = sh.signal()
toast:connect(function(msg) print(msg) end)
toast:fire("Saved")
Instance authoring
create(class) · create(class, templateName) · create(instance)
create(target: string | Instance, templateName?: string) → (props) → Instance
Returns a function that, given a props table, creates an instance. The first arg is a class name ("Frame") or an existing Instance to clone. The optional second arg picks a named template registered with SetTemplate.
sh.create("Frame") { Size = UDim2.fromScale(1, 1) }
sh.create("TextButton", "primary") { Text = "Go" }
sh.create(myTemplate) { Name = "Clone" }
Props are interpreted as:
- String keys set properties. If the value is a function, it becomes a reactive binding that re-evaluates when its sources change.
RBXScriptSignalkeys connect callbacks.Activated = fnworks becauseTextButton.Activatedis a signal property.- Numeric keys are children: instances, arrays of instances, action objects, or 0-arg functions returning instances (dynamic).
- The
Childrensentinel can be used as a key for an explicit children array.
apply(instance, props)
apply(instance: Instance, props: { [any]: any }) → Instance
Apply a props table to an existing instance. Same semantics as the props pass of create.
mount(component, parent?)
mount(component: () → Instance | { Instance }, parent?: Instance) → () → ()
Render component inside a fresh root and parent the result. Returns a destroy callback that disposes the entire subtree.
Children
An exported sentinel value. Use as a key when you want to pass children explicitly:
sh.create("Frame") {
Size = UDim2.fromScale(1, 1),
[sh.Children] = { btn1, btn2 },
}
action(fn, priority?)
action(fn: (Instance) → (), priority?: number) → Action
Wrap a callback to run against an instance after props and children are applied. Lower priority runs first; built-in helpers like ref use very negative priorities.
changed(property, callback)
changed(property: string, callback: (any) → ()) → Action
Mirror an instance property into a callback (or source) on every change.
local text = sh.source("")
sh.create("TextBox") { sh.changed("Text", text) }
portal(target, children)
portal(target: Instance, children: { any }) → Instance
Render children into an arbitrary target instance instead of the enclosing parent. Useful for tooltips and modals.
tag(name)
tag(name: string) → Action
Add a CollectionService tag, removed automatically on cleanup.
smoothLayout(opts?, springOpts?)
smoothLayout(opts?, springOpts?: { period?, damping? }) → Action
A drop-in replacement for UIListLayout that springs each child to its target slot. Supports FillDirection, Padding, SortOrder, HorizontalAlignment, and VerticalAlignment. A single shared Heartbeat drives every active layout.
sh.create("Frame") {
sh.smoothLayout({ Padding = UDim.new(0, 6) }, { period = 0.35, damping = 0.7 }),
-- ...children
}
onMount(fn)
onMount(fn: (Instance) → ()) → Action
Run fn on the next frame after the instance is parented into the live tree (so AbsoluteSize/AbsolutePosition are real).
onEvent(name, fn)
onEvent(name: string, fn: (...any) → ()) → Action
Connect to any RBXScriptSignal property by string name with auto-cleanup.
ref(target)
ref(target: ((Instance) → ()) | source) → Action
Capture an Instance reference into a callback or a source. Runs very early so other actions in the same props table can read the captured ref.
keybind(key, fn, opts?)
keybind(key: Enum.KeyCode, fn: (InputObject) → (), opts?: { gameProcessed?, ended? })
Attach a keyboard binding for the lifetime of the enclosing scope.
sh.keybind(Enum.KeyCode.Escape, close)
sh.keybind(Enum.KeyCode.E, interact, { gameProcessed = true })
hover()
hover() → (Action, () → boolean)
Returns an (action, source) pair. The action wires MouseEnter/MouseLeave to the boolean source.
local hAct, isHover = sh.hover()
sh.create("Frame") {
BackgroundTransparency = function() return isHover() and 0 or 0.2 end,
hAct,
}
pressed()
pressed() → (Action, () → boolean)
Like hover but tracks whether the user is currently holding the mouse / touch.
drag(initial?)
drag(initial?: UDim2) → (Action, () → UDim2)
Make a GuiObject draggable. Bind the returned source back to Position so the move sticks.
local d, pos = sh.drag()
sh.create("Frame") { Position = pos, d }
screenSize()
screenSize() → () → Vector2
A reactive source mirroring workspace.CurrentCamera.ViewportSize. Follows camera changes automatically.
fade(opts?)
fade(opts?: { duration?, easing?, direction?, fadeOut?, propertyMap? }) → Action
Tween transparency-style properties from 1 → original on mount and (optionally) back on cleanup. Auto-detects BackgroundTransparency, TextTransparency, and ImageTransparency by default.
Control flow
show(cond, component, fallback?)
show<T>(cond: () → any, component: () → T, fallback?: () → T) → () → T?
Conditionally renders a component based on the truthiness of cond. The component re-runs only when truthiness changes, so unrelated state updates don't recreate it.
switch(source)(cases)
switch<K>(source: () → K) → (cases: { [K]: () → any }) → () → any
Selects one component per source value. Components are memoized per key, so switching back is cheap.
sh.switch(route) {
home = HomePage,
profile = ProfilePage,
about = AboutPage,
}
match(source)(cases)
match<T>(source: () → T) → (cases: {{ predicate, component }}) → () → any
Like switch but matches via predicates instead of equality.
sh.match(state) {
{ function(s) return s.kind == "loading" end, Loading },
{ function(s) return s.kind == "ready" end, Ready },
}
indexes(list, component)
indexes<T>(list: () → { T }, component: (() → T, number) → any) → () → { any }
Render a list keyed by index. Each component receives a source yielding the current value at that position. Best when items are not reordered.
values(list, component)
values<T>(list: () → { T }, component: (T, () → number) → any) → () → { any }
Render a list keyed by value identity: rendered instances are preserved when items are reordered. Each component receives the value and a source yielding its index.
Animation
spring(target, period?, damping?)
spring<T>(target: () → T, period?: number, damping?: number) → () → T
Animated source driven by a critically-/under-damped harmonic oscillator. Tracks any source returning number, Vector2, Vector3, Color3, UDim, or UDim2. A single shared Heartbeat drives every active spring.
local pos = sh.source(UDim2.fromScale(0, 0))
local smooth = sh.spring(pos, 0.4, 0.7)
sh.create("Frame") { Position = smooth }
pos(UDim2.fromScale(1, 0)) -- glides smoothly across the screen
tween(target, info?)
tween<T>(target: () → T, info?: TweenInfo) → () → T
Animate a source toward a target using Roblox's TweenService easing curves. Returns a source that tracks the interpolated value.
Data
resource(fetcher)
resource<T>(fetcher: () → T) → (() → T?, ResourceStatus)
Asynchronous source. fetcher runs in a task; while pending the source returns nil and status.loading() is true. On error status.error() returns the thrown value. Re-runs whenever a tracked dependency changes; status.refetch() reruns manually.
local user, status = sh.resource(function()
return getUser(userId())
end)
sh.show(status.loading, Spinner, function()
return sh.create("TextLabel") {
Text = function() return user() and user().name or "" end,
}
end)
store(initial)
store<T>(initial: T) → Store<T>
A deeply-reactive table. Reading any nested field while in a tracking scope subscribes to that exact path. Writes via store:set(path, value) only notify subscribers of changed paths.
local s = sh.store({ user = { name = "Ada", age = 30 } })
sh.effect(function() print(s.user.name) end)
s:set({ "user", "name" }, "Grace") -- only the print re-runs
selector(source)
selector<K>(input: () → K) → (key: K) → () → boolean
Efficient single-key selector for highlight-style UIs. Given a source returning the "selected key", returns a function that yields a boolean source for any candidate key. Only the previously and currently selected keys' sources invalidate per change, giving O(1) updates instead of O(N) over the list.
local selected = sh.source("home")
local isSelected = sh.selector(selected)
-- inside a list component
TextColor3 = function() return isSelected("home")() and accent or fg end
debounce(source, delay)
debounce<T>(input: () → T, delay: number) → () → T
Wraps a source so downstream observers are only notified after delay seconds of quiet.
local query = sh.source("")
local debounced = sh.debounce(query, 0.25)
sh.effect(function() runSearch(debounced()) end)
throttle(source, interval)
throttle<T>(input: () → T, interval: number) → () → T
Like debounce, but emits the leading edge immediately and coalesces further changes within the window.
combine(...sources, fn)
combine(...sources, fn: (...) → R) → () → R
Combine N sources into one derived value. The last argument is the combiner.
local pos = sh.combine(x, y, function(xv, yv)
return Vector2.new(xv, yv)
end)
mapList(list, fn)
mapList<T, R>(list: () → { T }, fn: (T, number) → R) → () → { R }
Reactively map over an array source, caching transformed values per item identity (so unchanged items don't recompute).
Tooling
scheduler
Runtime hooks. Each method is init-gated.
scheduler.setStrict(boolean): re-run effects an extra time to surface side-effect bugs.scheduler.setOnError(fn): global handler for uncaught reactive errors.scheduler.flushSync(): drain pending updates immediately.scheduler.setDevtools({ onCreate, onDispose, onRun }): register devtools observer for graph nodes.