feat!: run listen and subscribe callback in untracked

This commit is contained in:
littensy 2026-03-19 18:42:30 -07:00
parent 50afcaf6e9
commit 5c27052965
11 changed files with 28 additions and 156 deletions

View file

@ -4,6 +4,6 @@ end
local Charm = require(script.Parent.Parent.charm.src)
export type Atom<T = any> = Charm.Atom<T>
export type Atom<T> = Charm.Atom<T>
return Charm

View file

@ -809,7 +809,7 @@ local function listen<T>(getter: Getter<T>, fn: (state: T, lastState: T?) -> ())
return effect(function()
local previous = current
current = tracked()
peek(fn, current, previous)
untracked(fn, current, previous)
end)
end
@ -837,7 +837,7 @@ local function subscribe<T>(getter: Getter<T>, fn: (state: T, lastState: T) -> (
if firstRun then
firstRun = false
else
peek(fn, current, previous)
untracked(fn, current, previous)
end
end)
end

View file

@ -3,7 +3,6 @@ local test = require("../../../tests/test")
local signal = Charm.signal
local listen = Charm.listen
local effect = Charm.effect
test("should listen to selector", function()
local source, setSource = signal(0)
@ -26,32 +25,23 @@ test("should listen to selector", function()
assert(current == 2 and previous == 1, "should not run after unsub")
end)
test("should run in peek", function()
test("should run in untracked", function()
local runs = 0
local cleanups = 0
local a, setA = signal(0)
local b, setB = signal(0)
listen(a, function()
runs += 1
b()
effect(function()
return function()
cleanups += 1
end
end)
end)
assert(runs == 1, "should run once on init")
assert(cleanups == 0, "should not cleanup yet")
setA(1)
assert(runs == 2, "should run on a change")
assert(cleanups == 1, "should cleanup first call")
assert(runs == 2, "should run once on change")
setB(1)
assert(runs == 2, "should not run on b change")
assert(cleanups == 1, "should not cleanup second call")
setA(2)
assert(runs == 3, "should run on a change")
end)
test("should run if first value is nil", function()
@ -67,36 +57,6 @@ test("should run if first value is nil", function()
assert(runs == 2, "should run once on change")
end)
test("should track inner subs", function()
local order = {}
local a, setA = signal(1)
local unsub = listen(a, function()
table.insert(order, 1)
listen(a, function()
table.insert(order, 2)
end)
listen(a, function()
table.insert(order, 3)
end)
end)
assert(table.concat(order) == "123", `bad mount order: {table.concat(order)}`)
table.clear(order)
setA(a() + 1)
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
table.clear(order)
setA(a() + 1)
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
table.clear(order)
unsub()
setA(a() + 1)
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
end)
test("should skip redundant updates", function()
local order = {}
local a, setA = signal(0)

View file

@ -4,8 +4,6 @@ local test = require("../../../tests/test")
local signal = Charm.signal
local computed = Charm.computed
local subscribe = Charm.subscribe
local listen = Charm.listen
local effect = Charm.effect
test("should subscribe to selector", function()
local source, setSource = signal(0)
@ -28,36 +26,23 @@ test("should subscribe to selector", function()
assert(current == 2 and previous == 1, "should not run after unsub")
end)
test("should run in peek", function()
test("should run in untracked", function()
local runs = 0
local cleanups = 0
local a, setA = signal(0)
local b, setB = signal(0)
subscribe(a, function()
runs += 1
b()
effect(function()
return function()
cleanups += 1
end
end)
end)
assert(runs == 0, "should not run once on init")
assert(cleanups == 0, "should not cleanup yet")
setA(1)
assert(runs == 1, "should run once on change")
assert(cleanups == 0, "should not cleanup yet")
setB(1)
assert(runs == 1, "should not run on b change")
assert(cleanups == 0, "should not cleanup first call")
setA(2)
assert(runs == 2, "should run on a change")
assert(cleanups == 1, "should cleanup first call")
end)
test("should ensure correct execution order", function()
@ -86,33 +71,6 @@ test("should ensure correct execution order", function()
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
end)
test("should track inner subs", function()
local order = {}
local a, setA = signal(1)
local unsub = subscribe(a, function()
table.insert(order, 1)
listen(a, function()
table.insert(order, 2)
end)
listen(a, function()
table.insert(order, 3)
end)
end)
setA(a() + 1)
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
table.clear(order)
setA(a() + 1)
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
table.clear(order)
unsub()
setA(a() + 1)
assert(table.concat(order) == "", `bad order: {table.concat(order)}`)
end)
test("should skip redundant updates", function()
local order = {}
local a, setA = signal(0)

View file

@ -2,8 +2,4 @@ if not script then
return require("../charm/src")
end
local Charm = require(script.Parent.Parent.charm.src)
export type Atom<T = any> = Charm.Atom<T>
return Charm
return require(script.Parent.Parent.charm.src)

View file

@ -1,5 +1 @@
local Charm = require(script.Parent.Parent.charm.src)
export type Atom<T = any> = Charm.Atom<T>
return Charm
return require(script.Parent.Parent.charm.src)

View file

@ -1,12 +1 @@
type CoreReactBinding<T> = {
getValue: (self: CoreReactBinding<T>) -> T,
_source: string?,
}
type ReactBindingMap = {
map: <T, U>(self: CoreReactBinding<T> & ReactBindingMap, (T) -> U) -> ReactBindingMap & CoreReactBinding<U>,
}
export type Binding<T> = CoreReactBinding<T> & ReactBindingMap
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").React)

View file

@ -17,10 +17,7 @@ local function useSignalState<T>(getter: () -> T, dependencies: { any }?): T
local value, setValue = React.useState(getter)
React.useLayoutEffect(function()
return Charm.effect(function()
-- Wrap in untracked to avoid tracking inner effect and atom calls
Charm.untracked(setValue, getter())
end)
return Charm.listen(getter, setValue)
end, dependencies or {})
return value
@ -42,10 +39,7 @@ local function useSignalBinding<T>(getter: () -> T, dependencies: { any }?)
local binding, setBinding = React.useBinding(initialValue)
React.useLayoutEffect(function()
return Charm.effect(function()
-- Wrap in untracked to avoid tracking inner effect and atom calls
Charm.untracked(setBinding, getter())
end)
return Charm.listen(getter, setBinding)
end, dependencies or {})
return binding

View file

@ -1,5 +1 @@
local Charm = require(script.Parent.Parent.charm.src)
export type Atom<T = any> = Charm.Atom<T>
return Charm
return require(script.Parent.Parent.charm.src)

View file

@ -11,10 +11,7 @@ local Vide = require("./Vide")
local function useSignalState<State>(getter: () -> State): () -> State
local state = Vide.source(getter())
Vide.cleanup(Charm.effect(function()
-- Wrap in untracked to avoid tracking inner effect and atom calls
Charm.untracked(state :: (State) -> State, getter())
end))
Vide.cleanup(Charm.listen(getter, state))
return state
end