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) },
})
Must be called before 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:

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.