refactor: remove unnecessary features, update docs

This commit is contained in:
littensy 2025-11-02 18:55:41 -08:00
parent 7a88c784fa
commit 37f79ecc3f
20 changed files with 774 additions and 955 deletions

View file

@ -141,7 +141,7 @@ export namespace client {
*/
export namespace server {
/**
* Subscribes a client to the given atoms. When an update occurs, the client
* Subscribes a client to the given signals. When an update occurs, the client
* will receive a partial state patch to merge into their local state. May be
* called multiple times to subscribe to additional keys.
*
@ -150,9 +150,9 @@ export namespace server {
* registering signals on the client should be done through `signalToAtom()`.
*
* @param client The client receiving state updates.
* @param atoms A map of atoms to sync with the client.
* @param signals A map of signals to sync with the client.
*/
export function subscribeClient(client: Player, atoms: { [key: string]: Atom<any> }): void;
export function subscribeClient(client: Player, signals: { [key: string]: () => any }): void;
/**
* Unsubscribes a client from receiving all state updates. To only unsubscribe
@ -277,3 +277,14 @@ export function flattenAtoms<Atoms extends NestedAtomMap>(atoms: Atoms): Flatten
* @returns `true` if the value is `None`, otherwise `false`.
*/
export function isNone(value: unknown): value is None;
/**
* Converts a signal getter and setter into an atom. This is useful for
* syncing client signals with the server, as the sync system requires both
* the getter and setter to update state.
*
* @param getter A function that returns the current value of the signal.
* @param setter A function that sets the value of the signal.
* @returns An atom that can either get or set the signal's value.
*/
export function signalToAtom<T>(getter: () => T, setter: (value: ((prev: T) => T) | T) => T): Atom<T>;

View file

@ -9,7 +9,7 @@ local globals = Charm.globals
type SyncStatus = number
type ClientAtoms = {
type ClientKeys = {
[string]: SyncStatus,
}
@ -29,17 +29,17 @@ type StateUpdate = {
local PENDING_INITIAL_STATE: SyncStatus = 0
local LISTENING_FOR_CHANGES: SyncStatus = 1
local trackedAtoms: { [string]: TrackedState } = {}
local trackedSignals: { [string]: TrackedState } = {}
local globalUpdates: { StateUpdate } = {}
local clients: { [any]: ClientAtoms } = {}
local clients: { [any]: ClientKeys } = {}
local connection: RBXScriptConnection?
local syncCallback: (client: any, payloads: { SyncPayload }) -> ()?
local function startTracking<T>(key: string, getter: () -> T)
if trackedAtoms[key] then
if trackedSignals[key] then
-- Ignore duplicates, but warn against name collisions
if trackedAtoms[key].getter ~= getter then
if trackedSignals[key].getter ~= getter then
warn("[Charm] Attempted to sync different states with the same key:", key)
end
return
@ -68,27 +68,27 @@ local function startTracking<T>(key: string, getter: () -> T)
trackedState.currentValue = newValue
end)
trackedAtoms[key] = trackedState
trackedSignals[key] = trackedState
end
local function stopTracking(key: string)
local tracker = trackedAtoms[key]
local tracker = trackedSignals[key]
if tracker then
tracker.dispose()
trackedAtoms[key] = nil
trackedSignals[key] = nil
end
end
local function purgeUntracked()
local active: { [string]: boolean } = {}
for _, clientAtoms in clients do
for key in clientAtoms do
for _, clientKeys in clients do
for key in clientKeys do
active[key] = true
end
end
for key in trackedAtoms do
for key in trackedSignals do
if not active[key] then
stopTracking(key)
end
@ -107,9 +107,9 @@ local function diffGlobalUpdateBuffer(): { any }
end
local function selectFromGlobalPatchList(client: any, globalPatches: { any }): { SyncPayload }
local clientAtoms = clients[client]
local clientKeys = clients[client]
if not clientAtoms then
if not clientKeys then
return {}
end
@ -125,7 +125,7 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
end
local key = update.key
local status = clientAtoms[key]
local status = clientKeys[key]
if status == LISTENING_FOR_CHANGES then
table.insert(payloads, {
@ -137,20 +137,20 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
type = "init",
data = { [key] = update.currentValue },
})
clientAtoms[key] = LISTENING_FOR_CHANGES
clientKeys[key] = LISTENING_FOR_CHANGES
end
end
-- If a pending atom was never updated, we still need to send its initial
-- state. This can happen if the atom was registered after its last change.
-- If a pending signal was never updated, we still need to send its initial
-- state. This can happen if a signal was registered after its last change.
local initData: { [string]: any } = {}
for key, status in clientAtoms do
local trackedState = trackedAtoms[key]
for key, status in clientKeys do
local trackedState = trackedSignals[key]
if status == PENDING_INITIAL_STATE and trackedState then
initData[key] = trackedState.currentValue
clientAtoms[key] = LISTENING_FOR_CHANGES
clientKeys[key] = LISTENING_FOR_CHANGES
end
end
@ -165,7 +165,7 @@ local function diffGlobalState(): any
local currentGlobalState: { [string]: any } = {}
local previousGlobalState: { [string]: any } = {}
for key, tracker in trackedAtoms do
for key, tracker in trackedSignals do
currentGlobalState[key] = tracker.currentValue
previousGlobalState[key] = tracker.previousValue
tracker.previousValue = tracker.currentValue
@ -175,9 +175,9 @@ local function diffGlobalState(): any
end
local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPayload }
local clientAtoms = clients[client]
local clientKeys = clients[client]
if not clientAtoms then
if not clientKeys then
return {}
end
@ -185,16 +185,16 @@ local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPaylo
local initData: { [string]: any } = {}
local patchData: { [string]: any } = {}
-- Create patches for initialized atoms that changed, and add atoms that
-- are still pending initial state to the init payload.
for key, status in clientAtoms do
local trackedState = trackedAtoms[key]
-- Create patches for initialized signals that changed, and add signals
-- that are still pending initial state to the init payload.
for key, status in clientKeys do
local trackedState = trackedSignals[key]
if status == LISTENING_FOR_CHANGES then
patchData[key] = globalPatch[key]
elseif status == PENDING_INITIAL_STATE then
initData[key] = trackedState.currentValue
clientAtoms[key] = LISTENING_FOR_CHANGES
clientKeys[key] = LISTENING_FOR_CHANGES
end
end
@ -209,7 +209,7 @@ local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPaylo
return payloads
end
--- Subscribes a client to the given atoms. When an update occurs, the client
--- Subscribes a client to the given signals. When an update occurs, the client
--- will receive a partial state patch to merge into their local state. May be
--- called multiple times to subscribe to additional keys.
---
@ -218,21 +218,21 @@ end
--- registering signals on the client should be done through `signalToAtom()`.
---
--- @param client The client receiving state updates.
--- @param atoms A map of atoms to sync with the client.
local function subscribeClient(client: any, atoms: { [string]: () -> any })
--- @param signals A map of signals to sync with the client.
local function subscribeClient(client: any, signals: { [string]: () -> any })
if not clients[client] then
clients[client] = {}
end
local clientAtoms = clients[client]
local clientKeys = clients[client]
for key, state in atoms do
for key, state in signals do
startTracking(key, if type(state) == "function" then state else state.get)
if not clientAtoms[key] then
-- Mark the atom as pending initial state so we send its full value
-- the next time we sync.
clientAtoms[key] = PENDING_INITIAL_STATE
if not clientKeys[key] then
-- Mark the signal as pending initial state so we send its full
-- value the next time we sync.
clientKeys[key] = PENDING_INITIAL_STATE
end
end
end
@ -250,18 +250,18 @@ end
--- @param client The client receiving state updates.
--- @param keys The keys of the state to unsubscribe from.
local function unsubscribeClientFrom(client: any, ...: string)
local clientAtoms = clients[client]
local clientKeys = clients[client]
if not clientAtoms then
if not clientKeys then
return
end
for index = 1, select("#", ...) do
local key = select(index, ...)
clientAtoms[key] = nil
clientKeys[key] = nil
end
if next(clientAtoms) == nil then
if next(clientKeys) == nil then
clients[client] = nil
end
end
@ -346,7 +346,7 @@ end
--- Unsubscribes all clients, stops tracking all state, and disconnects
--- from the automatic sync interval.
local function reset()
table.clear(trackedAtoms)
table.clear(trackedSignals)
table.clear(clients)
table.clear(globalUpdates)
disconnect()

View file

@ -40,6 +40,13 @@ local function flattenState(states: NestedState): FlattenedState
return result
end
--- Converts a signal getter and setter into an atom. This is useful for
--- syncing client signals with the server, as the sync system requires both
--- the getter and setter to update state.
---
--- @param getter A function that returns the current value of the signal.
--- @param setter A function that sets the value of the signal.
--- @return An atom that can either get or set the signal's value.
local function signalToAtom<T>(getter: () -> T, setter: (value: ((T) -> T) | T) -> T): Atom<T>
return function(...)
return if select("#", ...) == 0 then getter() else setter((...))

View file

@ -354,25 +354,4 @@ test("should run cleanups outside any evaluation context", function()
assert(calls == 0, "should not run after b update")
end)
test("should allow self cleanup", function()
local calls = 0
local cleanups = 0
local a, setA = signal(0)
effect(function(stopEffect)
onCleanup(function()
cleanups += 1
end)
stopEffect()
a()
calls += 1
end)
assert(calls == 1, "should call effect once")
assert(cleanups == 1, "should call cleanup once")
setA(1)
assert(calls == 1, "should not call effect after a update")
assert(cleanups == 1, "should not call cleanup after a update")
end)
return {}

View file

@ -4,7 +4,6 @@ local suite = require("../../../../test/suite")
local signal = Charm.signal
local listen = Charm.listen
local effect = Charm.effect
local onCleanup = Charm.onCleanup
local test = suite.test
test("should listen to selector", function()
@ -116,24 +115,4 @@ test("should skip redundant updates", function()
assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`)
end)
test("should allow self cleanup", function()
local calls = 0
local cleanups = 0
local a, setA = signal(0)
listen(a, function(_, _, stopEffect)
onCleanup(function()
cleanups += 1
end)
stopEffect()
calls += 1
end)
assert(calls == 1, "should call effect once")
assert(cleanups == 1, "should call cleanup once")
setA(1)
assert(calls == 1, "should not call effect after a update")
assert(cleanups == 1, "should not call cleanup after a update")
end)
return {}

View file

@ -51,9 +51,8 @@ type EffectScope = ReactiveNode & {
}
type Effect = ReactiveNode & {
fn: (stop: () -> ()) -> (() -> ())?,
fn: () -> (() -> ())?,
cleanups: { () -> () },
stop: () -> (),
}
type Computed<T = any> = ReactiveNode & {
@ -117,7 +116,7 @@ local function run(e: Effect): ()
e.depsTail = nil
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
local prevSub = setActiveSub(e)
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(e.fn, e.stop)
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(e.fn)
activeSub = prevSub
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
purgeDeps(e)
@ -197,7 +196,7 @@ local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
end
local function computedOper<T>(this: Computed<T>): () -> T
return function(): T
local function computedGetter(): T
local flags = this.flags
if
bit32.btest(flags, DIRTY)
@ -225,13 +224,14 @@ local function computedOper<T>(this: Computed<T>): () -> T
end
return this.value :: T
end
return computedGetter
end
local function signalOper<T>(
this: Signal<T>,
equals: ((current: T, incoming: T) -> boolean)?
): (() -> T, (((T) -> T) | T) -> T)
local function setter(update: ((T) -> T) | T): T
local function signalSetter(update: ((T) -> T) | T): T
local value: T
if type(update) == "function" then
local prevSub = setActiveSub(nil)
@ -262,7 +262,7 @@ local function signalOper<T>(
return this.currentValue
end
local function getter(): T
local function signalGetter(): T
if bit32.btest(this.flags, DIRTY) then
if updateSignal(this) then
local subs = this.subs
@ -285,7 +285,7 @@ local function signalOper<T>(
return this.currentValue
end
return getter, setter
return signalGetter, signalSetter
end
local function effectScopeOper(this: EffectScope): () -> ()
@ -311,7 +311,7 @@ end
--- @param initialValue The initial value of the signal.
--- @param equals A comparator function to determine if the signal's value has changed.
--- @return A tuple containing the getter and setter functions for the signal.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#signalstate-equals
--- @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
local function signal<T>(
initialValue: T,
equals: ((current: T, incoming: T) -> boolean)?
@ -326,30 +326,24 @@ local function signal<T>(
}, equals)
end
--- Creates a reactive atom that acts as both a getter and setter. Calling the
--- atom with an argument sets its value, while calling it with no arguments
--- returns its current value.
--- Creates a reactive atom that acts as both a signal getter and setter.
--- Calling the atom with an argument sets its value, while calling it with no
--- arguments returns its current value.
---
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
--- @param initialValue The initial value of the atom. If empty, the atom's value will be `nil`.
--- @param equals A comparator function to determine if the atom's value has changed.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-equals
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
local getter, setter = signal(initialValue, equals)
return function(...)
local function atomGetterSetter(...)
return if select("#", ...) == 0 then getter() else setter((...))
end
return atomGetterSetter
end
--- Creates a new atom that is computed based on the values of other atoms.
--- The returned atom is read-only, and its value is automatically updated when
--- any atoms accessed from within the getter function change.
---
--- @example
--- ```ts
--- const count = atom(1);
--- const plusOne = computed(() => count() + 1);
--- print(plusOne()); // 2
--- ```
--- Creates a new read-only signal that is computed based on the values of
--- other signals. The computed signal's value is automatically updated when a
--- change is detected in any of its dependencies.
---
--- @param getter A function that produces the next value.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
@ -360,50 +354,41 @@ local function computed<T>(getter: (previousValue: T?) -> T): () -> T
})
end
--- Creates an effect that runs a callback in response to atom state changes.
--- An effect tracks which atoms are accessed during its execution, and re-runs
--- the callback when those atoms change.
--- Creates an effect that runs a callback in response to signal state changes.
--- An effect tracks which signals are accessed during its execution, and runs
--- the callback when those signals change.
---
--- The effect callback may return a cleanup function, which gets called once,
--- either before the effect re-runs or when it is disposed.
---
--- If the effect is called within another effect, it will be disposed when the
--- outer effect re-runs.
--- parent effect re-runs.
---
--- @param fn The function to run when dependencies change.
--- @return A function for disposing the effect.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
local function effect(fn: (stop: () -> ()) -> ()): () -> ()
local function effect(fn: () -> ()): () -> ()
local e: Effect = {
fn = fn,
flags = WATCHING,
cleanups = {},
stop = nil :: any,
}
e.stop = effectOper(e)
local prevSub = setActiveSub(e)
if prevSub and (globals.trackInnerEffects or prevSub.flags == NONE) then
link(e, prevSub, 0)
end
local stopped = false
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn, function()
stopped = true
end)
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn)
activeSub = prevSub
if not success then
error(result)
elseif result then
table.insert(e.cleanups, result)
end
if stopped then
e.stop()
end
return e.stop
return effectOper(e)
end
--- Creates an effect scope that can capture multiple reactive effects (i.e.
--- computed, effect, subscribe) created within it so that they can be disposed
--- together.
--- Creates an effect scope that can capture reactive effects and computed
--- signals created within it so that they can be disposed together.
---
--- @param fn A function that may create reactive effects.
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
@ -428,12 +413,12 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
return effectScopeOper(e)
end
--- Registers a cleanup function to the current active effect or effect scope.
--- If there is no active effect or scope, a warning is logged and the callback
--- is not registered, unless `failSilently` is set to true.
--- Binds the cleanup function to the current active effect or effect scope. If
--- there is no active effect, a warning is logged, unless `failSilently` is
--- set to `true`.
---
--- @param callback The cleanup function to register.
--- @param failSilently If true, suppresses the warning when there is no active effect or scope. Defaults to false.
--- @param failSilently If `true`, suppresses the warning when there is no active effect or scope. Defaults to `false`.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
if activeSub and (activeSub :: Effect).cleanups then
@ -443,7 +428,7 @@ local function onCleanup(cleanup: () -> (), failSilently: boolean?)
end
end
--- Runs the callback without subscribing to atom updates or capturing inner
--- Runs the callback without subscribing to signal updates or capturing inner
--- effects in the parent effect or scope.
---
--- For an alternative that still cleans up inner effects, use `peek` instead.
@ -462,7 +447,7 @@ local function untracked<Args..., Results...>(callback: (Args...) -> Results...,
return unpack(results, 2)
end
--- Runs the callback without subscribing to atom updates, but still captures
--- Runs the callback without subscribing to signal updates, but still captures
--- inner effects so that they dispose with the parent effect.
---
--- To avoid capturing inner effects, use `untracked` instead.
@ -489,10 +474,10 @@ local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...:
end
end
--- Combines multiple atom updates into a single "commit" that runs effects at
--- the end of the provided callback.
--- Combines multiple updates into a single "commit" that runs effects and
--- computed signals after the provided callback finishes running.
---
--- @param callback A function that performs multiple atom updates.
--- @param callback A function that performs multiple updates.
--- @param args Arguments to pass to the callback.
--- @return The return value of the callback.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
@ -506,18 +491,56 @@ local function batched<Args..., Results...>(callback: (Args...) -> Results..., .
return unpack(results, 2)
end
--- Converts a non-signal into a computed signal. If the callback is already a
--- signal, it is returned as-is. Useful for memoizing a getter function
--- without creating unnecessary nested signals.
---
--- @param getter A function that returns the value to track.
--- @return A computed signal that tracks the value returned by the getter.
local function toComputed<T>(getter: () -> T): () -> T
local name = debug.info(getter, "n")
if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then
return getter
end
return computed(function()
return getter()
end)
end
--- Creates an effect that runs the callback one immediately, and then again
--- when the value returned by the getter changes. The callback receives the new
--- value and the previous value as arguments.
---
--- Effects created within the callback are cleaned up when the listener runs
--- or the listener is disposed.
---
--- @param getter A function that returns the value to watch.
--- @param callback The function to run when the value changes.
--- @return A function for disposing the effect.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?) -> ()): () -> ()
local tracked = toComputed(getter)
local current: T
return effect(function()
local previous = current
current = tracked()
peek(callback, current, previous)
end)
end
--- Creates an effect that only runs the callback when the value returned by the
--- getter changes. The callback receives the new value and the previous value
--- as arguments.
---
--- Effects created within the callback are cleaned up when the listener runs
--- or the listener is disposed.
---
--- @param getter A function that returns the value to watch.
--- @param callback The function to run when the value changes.
--- @return A function for disposing the effect.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
local tracked = computed(function()
return getter()
end)
local tracked = toComputed(getter)
local current: T
local mounted = true
return effect(function()
@ -531,34 +554,6 @@ local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T)
end)
end
--- Creates an effect that runs the callback one immediately, and then again
--- when the value returned by the getter changes. The callback receives the new
--- value, the previous value, and a cleanup function as arguments.
---
--- @param getter A function that returns the value to watch.
--- @param callback The function to run when the value changes.
--- @return A function for disposing the effect.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
local tracked = computed(function()
return getter()
end)
local current: T
local disposed = false
local dispose = function()
disposed = true
end
dispose = effect(function()
local previous = current
current = tracked()
peek(callback, current, previous, dispose)
end)
if disposed then
dispose()
end
return dispose
end
local reactiveSystem = createReactiveSystem({
update = function(node)
if node.depsTail then

View file

@ -6,11 +6,11 @@ local isO2 = inlineTest() ~= "inlineTest"
local isDev = not isO2 or _G.__DEV__ == true
return {
--- Enforces synchronous, non-yielding behavior in atoms and effects.
--- Enforces synchronous, non-yielding behavior in signals and effects.
--- Also enables state validation in Charm Sync to catch sync errors early.
--- Enabled if the optimization level is below 2, which is true in studio.
strict = isDev,
--- Enforces immutability of atoms by deep-freezing table values. Enabled
--- Enforces immutability of tables by deep-freezing table values. Enabled
--- if the optimization level is below 2, which is true in studio.
frozen = isDev,
--- Whether parent effects should track inner effects and clean them up

View file

@ -20,13 +20,13 @@ export type Equals<T> = (current: T, incoming: T) => boolean;
*/
export const globals: {
/**
* Enforces synchronous, non-yielding behavior in atoms and effects.
* Enforces synchronous, non-yielding behavior in signals and effects.
* Also enables state validation in Charm Sync to catch sync errors early.
* Enabled if the optimization level is below 2, which is true in studio.
*/
strict: boolean;
/**
* Enforces immutability of atoms by deep-freezing table values. Enabled
* Enforces immutability of tables by deep-freezing table values. Enabled
* if the optimization level is below 2, which is true in studio.
*/
frozen: boolean;
@ -44,84 +44,138 @@ export const globals: {
*
* @param initialValue The initial value of the signal.
* @param equals A comparator function to determine if the signal's value has changed.
* @return A tuple containing the getter and setter functions for the signal.
* @see https://github.com/littensy/charm?tab=readme-ov-file#signalstate-equals
* @returns A tuple containing the getter and setter functions for the signal.
* @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
*/
export function signal<T>(
value: T,
initialValue: T,
equals?: Equals<T>,
): LuaTuple<[() => T, (newValue: T | ((currentValue: T) => T)) => T]>;
): LuaTuple<[getter: () => T, setter: (newValue: T | ((currentValue: T) => T)) => T]>;
export function signal<T>(): LuaTuple<
[() => T | undefined, (newValue: T | ((currentValue?: T) => T | undefined)) => T | undefined]
[getter: () => T | undefined, setter: (newValue: T | ((currentValue?: T) => T | undefined)) => T | undefined]
>;
/**
* Creates a reactive atom that acts as both a getter and setter. Calling the
* atom with an argument sets its value, while calling it with no arguments
* returns its current value.
* Creates a reactive atom that acts as both a signal getter and setter.
* Calling the atom with an argument sets its value, while calling it with no
* arguments returns its current value.
*
* @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
* @param initialValue The initial value of the atom. If empty, the atom's value will be `nil`.
* @param equals A comparator function to determine if the atom's value has changed.
* @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
* @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals
*/
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
export function atom<T>(initialValue: T, equals?: Equals<T>): Atom<T>;
export function atom<T>(): Atom<T | undefined>;
/**
* Creates a new atom that is computed based on the values of other atoms.
* The returned atom is read-only, and its value is automatically updated when
* any atoms accessed from within the getter function change.
*
* If an initial value is provided, the getter will receive this value as its
* first argument on the initial run, instead of `undefined`.
*
* @example
* ```ts
* const count = atom(1);
* const plusOne = computed(() => count() + 1);
* print(plusOne()); // 2
* ```
* Creates a new read-only signal that is computed based on the values of
* other signals. The computed signal's value is automatically updated when a
* change is detected in any of its dependencies.
*
* @param getter A function that produces the next value.
* @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
* @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
*/
export function computed<T>(getter: (previousValue?: T) => T): () => T;
/**
* Creates an effect that runs a callback in response to atom state changes.
* An effect tracks which atoms are accessed during its execution, and re-runs
* the callback when those atoms change.
* Creates an effect that runs a callback in response to signal state changes.
* An effect tracks which signals are accessed during its execution, and runs
* the callback when those signals change.
*
* The effect callback may return a cleanup function, which gets called once,
* either before the effect re-runs or when it is disposed.
*
* If the effect is called within another effect, it will be disposed when the
* outer effect re-runs.
* parent effect re-runs.
*
* @param callback The function to run when dependencies change.
* @param fn The function to run when dependencies change.
* @returns A function for disposing the effect.
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
*/
export function effect(callback: (dispose: Cleanup) => Cleanup | void): Cleanup;
export function effect(callback: () => Cleanup | void): Cleanup;
/**
* Creates an effect scope that can capture multiple reactive effects (i.e.
* computed, effect, subscribe) created within it so that they can be disposed
* together.
* Creates an effect scope that can capture reactive effects and computed
* signals created within it so that they can be disposed together.
*
* @param callback A function that may create reactive effects.
* @param detached If true, the scope will not be automatically disposed of
* when the parent scope is disposed. Defaults to false.
* @param fn A function that may create reactive effects.
* @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
* @returns A function for disposing all effects created within the scope.
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
*/
export function effectScope(callback: () => Cleanup | void, detached?: boolean): Cleanup;
/**
* Binds the cleanup function to the current active effect or effect scope. If
* there is no active effect, a warning is logged, unless `failSilently` is
* set to `true`.
*
* @param callback The cleanup function to register.
* @param failSilently If `true`, suppresses the warning when there is no active effect or scope. Defaults to `false`.
* @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
*/
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
/**
* Runs the callback without subscribing to signal updates or capturing inner
* effects in the parent effect or scope.
*
* For an alternative that still cleans up inner effects, use `peek` instead.
*
* @param callback The callback function.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
* @see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
*/
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Runs the callback without subscribing to signal updates, but still captures
* inner effects so that they dispose with the parent effect.
*
* To avoid capturing inner effects, use `untracked` instead.
*
* @param callback The callback function.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
* @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
*/
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Combines multiple updates into a single "commit" that runs effects and
* computed signals after the provided callback finishes running.
*
* @param callback A function that performs multiple updates.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
*/
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Creates an effect that runs the callback one immediately, and then again
* when the value returned by the getter changes. The callback receives the new
* value and the previous value as arguments.
*
* Effects created within the callback are cleaned up when the listener runs
* or the listener is disposed.
*
* @param getter A function that returns the value to watch.
* @param callback The function to run when the value changes.
* @returns A function for disposing the effect.
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
*/
export function listen<T>(getter: () => T, callback: (value: T, previousValue: T | undefined) => void): Cleanup;
/**
* Creates an effect that only runs the callback when the value returned by the
* getter changes. The callback receives the new value and the previous value
* as arguments.
*
* Effects created within the callback are cleaned up when the listener runs
* or the listener is disposed.
*
* @param getter A function that returns the value to watch.
* @param callback The function to run when the value changes.
* @returns A function for disposing the effect.
@ -130,28 +184,12 @@ export function effectScope(callback: () => Cleanup | void, detached?: boolean):
export function subscribe<T>(getter: () => T, callback: (value: T, previousValue: T) => void): Cleanup;
/**
* Creates an effect that runs the callback one immediately, and then again
* when the value returned by the getter changes. The callback receives the new
* value, the previous value, and a cleanup function as arguments.
* Creates a new read-only signal that is computed by mapping over the entries
* of an existing atom. If the key is omitted from the mapper's return value,
* the original key is preserved.
*
* @param getter A function that returns the value to watch.
* @param callback The function to run when the value changes.
* @returns A function for disposing the effect.
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
*/
export function listen<T>(
getter: () => T,
callback: (value: T, previousValue: T | undefined, dispose: Cleanup) => void,
): Cleanup;
/**
* Creates a new atom that is computed by mapping over the entries of an
* existing atom. The returned atom is read-only, and its value is
* automatically updated when the source atom changes.
*
* @param getter A function that returns the table to map over.
* @param mapper A function that transforms each value-key pair from the source
* into a new value or value-key pair for the resulting table.
* @param getter Returns the table to map over.
* @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
*/
export function mapped<VI, KI, VO, KO = KI>(
@ -170,70 +208,27 @@ export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
): () => { readonly [K in KO]: VO };
/**
* Calls the observer function for each unique key in the table returned by the
* getter. The observer is called once for each key, and again when a new key
* Calls the callback function for each unique key in the table returned by the
* getter. The callback is called once for each key, and again when a new key
* is added.
*
* The observer may return a cleanup function, which will be called when the
* key is removed or when the entire observer is disposed.
* The callback may return a cleanup function, which will be called when the
* key is removed or when the entire callback is disposed.
*
* Effects created within the callback are cleaned up when the item is removed
* from the table or when the callback is disposed.
*
* @param getter A function that returns the table to observe.
* @param observer A function that is called for each key in the table.
* @param callback A function that is called for each key in the table.
* @returns A function for disposing the observer.
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
*/
export function observe<V, K = number>(
getter: () => Map<K, V> | ReadonlyMap<K, V> | readonly V[],
observer: (value: V, key: K) => Cleanup | void,
callback: (value: V, key: K) => Cleanup | void,
): Cleanup;
// Overload for objects
export function observe<V, K extends Key>(
getter: () => { readonly [K in Key]: V },
observer: (value: V, key: K) => Cleanup | void,
callback: (value: V, key: K) => Cleanup | void,
): Cleanup;
/**
* Combines multiple atom updates into a single "commit" that runs effects at
* the end of the provided callback.
*
* @param callback A function that performs multiple atom updates.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
*/
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Runs the callback without subscribing to atom updates or capturing inner
* effects in the parent effect or scope.
*
* For an alternative that still cleans up inner effects, use `peek` instead.
*
* @param callback The callback function.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
*/
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Runs the callback without subscribing to atom updates, but still captures
* inner effects so that they dispose with the parent effect.
*
* To avoid capturing inner effects, use `untracked` instead.
*
* @param callback The callback function.
* @param args Arguments to pass to the callback.
* @returns The return value of the callback.
*/
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
/**
* Registers a cleanup function to the current active effect or effect scope.
* If there is no active effect or scope, a warning is logged and the callback
* is not registered, unless `failSilently` is set to true.
*
* @param callback The cleanup function to register.
* @param failSilently If true, suppresses the warning when there is no active
* effect or scope. Defaults to false.
* @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
*/
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;

View file

@ -8,7 +8,7 @@ type Mapped =
& (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
local currentData: { [KI]: VI } = {}
local currentInput: { [KI]: VI } = {}
local keyCache: { [KI]: KO } = {}
local function map(value: VI, key: KI): (VO, KO)
@ -25,38 +25,38 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
return computed(function(previousOutput)
local currentOutput: { [KO]: VO } = previousOutput or {}
local previousData = currentData
currentData = getter()
local previousInput = currentInput
currentInput = getter()
for key in previousData do
if currentData[key] ~= nil then
for keyIn in previousInput do
if currentInput[keyIn] ~= nil then
continue
end
local cachedKey = keyCache[key]
local prevKeyOut = keyCache[keyIn]
if cachedKey then
if prevKeyOut then
if currentOutput == previousOutput then
currentOutput = table.clone(currentOutput)
end
currentOutput[cachedKey] = nil
keyCache[key] = nil
currentOutput[prevKeyOut] = nil
keyCache[keyIn] = nil
end
end
for key, value in currentData do
if value == previousData[key] then
for keyIn, valueIn in currentInput do
if valueIn == previousInput[keyIn] then
continue
end
local valueOut, keyOut = map(value, key)
local valueOut, keyOut = map(valueIn, keyIn)
if currentOutput[keyOut] ~= valueOut then
if currentOutput == previousOutput then
currentOutput = table.clone(currentOutput)
end
currentOutput[keyOut] = valueOut
keyCache[key] = keyOut
keyCache[keyIn] = keyOut
end
end
@ -64,12 +64,12 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
end)
end
--- Creates a new atom that is computed by mapping over the entries of an
--- existing atom. The returned atom is read-only, and its value is
--- automatically updated when the source atom changes.
--- Creates a new read-only signal that is computed by mapping over the entries
--- of an existing atom. If the key is omitted from the mapper's return value,
--- the original key is preserved.
---
--- @param getter A function that returns the table to map over.
--- @param mapper A function that transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
--- @param getter Returns the table to map over.
--- @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
local mapped = mapped :: Mapped

View file

@ -6,18 +6,21 @@ local effect = core.effect
local effectScope = core.effectScope
local onCleanup = core.onCleanup
--- Calls the observer function for each unique key in the table returned by the
--- getter. The observer is called once for each key, and again when a new key
--- Calls the callback function for each unique key in the table returned by the
--- getter. The callback is called once for each key, and again when a new key
--- is added.
---
--- The observer may return a cleanup function, which will be called when the
--- key is removed or when the entire observer is disposed.
--- The callback may return a cleanup function, which will be called when the
--- key is removed or when the entire callback is disposed.
---
--- Effects created within the callback are cleaned up when the item is removed
--- from the table or when the callback is disposed.
---
--- @param getter A function that returns the table to observe.
--- @param observer A function that is called for each key in the table.
--- @param callback A function that is called for each key in the table.
--- @return A function for disposing the observer.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key: K) -> ...(() -> ())?): () -> ()
local scopes: { [K]: () -> () } = {}
local currentItems: { [K]: V } = {}
@ -33,7 +36,7 @@ local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key:
for key, value in currentItems do
if previousItems[key] == nil and scopes[key] == nil then
scopes[key] = effectScope(function()
return observer(value, key)
return callback(value, key)
end, true)
end
end

View file

@ -1,11 +1,11 @@
local suite = require("../../../../test/suite")
suite.describe("useAtom", function()
require("./useAtom.spec")
suite.describe("useSignalState", function()
require("./useSignalState.spec")
end)
suite.describe("useAtomBinding", function()
require("./useAtomBinding.spec")
suite.describe("useSignalBinding", function()
require("./useSignalBinding.spec")
end)
return {}

View file

@ -3,45 +3,43 @@ local ReactCharm = require("../")
local renderHook = require("./helpers/renderHook")
local suite = require("../../../../test/suite")
local atom = Charm.atom
local useAtomBinding = ReactCharm.useAtomBinding
local test = suite.test
test("tracks atom state", function()
local state = atom(0)
test("tracks signal state", function()
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtomBinding(state)
return ReactCharm.useSignalBinding(getState)
end)
assert(hook.result:getValue() == 0, "should return 0")
state(1)
setState(1)
assert(hook.result:getValue() == 1, "should return 1")
end)
test("accepts a selector", function()
local state = atom(0)
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtomBinding(function()
return 2 * state()
return ReactCharm.useSignalBinding(function()
return 2 * getState()
end)
end)
assert(hook.result:getValue() == 0, "should return 0")
state(1)
setState(1)
assert(hook.result:getValue() == 2, "should return 2")
end)
test("tracks dependencies", function()
local state = atom(0)
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtomBinding(state, { state })
return ReactCharm.useSignalBinding(getState, { getState })
end)
assert(hook.result:getValue() == 0, "should return 0")
state(1)
setState(1)
assert(hook.result:getValue() == 1, "should return 1")
state = atom(0)
getState, setState = Charm.signal(0)
hook.rerender()
assert(hook.result:getValue() == 0, "should return 0")
state(1)
setState(1)
assert(hook.result:getValue() == 1, "should return 1")
end)

View file

@ -3,45 +3,43 @@ local ReactCharm = require("../")
local renderHook = require("./helpers/renderHook")
local suite = require("../../../../test/suite")
local atom = Charm.atom
local useAtom = ReactCharm.useAtom
local test = suite.test
test("tracks atom state", function()
local state = atom(0)
test("tracks signal state", function()
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtom(state)
return ReactCharm.useSignalState(getState)
end)
assert(hook.result == 0, "should return 0")
state(1)
setState(1)
assert(hook.result == 1, "should return 1")
end)
test("accepts a selector", function()
local state = atom(0)
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtom(function()
return 2 * state()
return ReactCharm.useSignalState(function()
return 2 * getState()
end)
end)
assert(hook.result == 0, "should return 0")
state(1)
setState(1)
assert(hook.result == 2, "should return 2")
end)
test("tracks dependencies", function()
local state = atom(0)
local getState, setState = Charm.signal(0)
local hook = renderHook(function()
return useAtom(state, { state })
return ReactCharm.useSignalState(getState, { getState })
end)
assert(hook.result == 0, "should return 0")
state(1)
setState(1)
assert(hook.result == 1, "should return 1")
state = atom(0)
getState, setState = Charm.signal(0)
hook.rerender()
assert(hook.result == 0, "should return 0")
state(1)
setState(1)
assert(hook.result == 1, "should return 1")
end)

View file

@ -1,29 +1,31 @@
import { Binding } from "@rbxts/react";
/**
* A hook that subscribes to changes in the given atom or selector. The
* component is re-rendered with the new state after an update.
* Returns a React state that updates when the getter function updates.
* Re-renders the component when the value changes.
*
* If the `dependencies` array is provided, a new effect is created for the
* current `callback` when the dependencies change. Otherwise, the effect is
* current `getter` when the dependencies change. Otherwise, the effect is
* created only once, when the component is mounted.
*
* @param callback The atom or selector to watch.
* @param dependencies An array of values used to memoize the callback.
* @returns The current state.
* @param getter The signal or selector to watch.
* @param dependencies An array of values used to memoize the getter.
* @return The current state.
*/
export function useAtom<T>(callback: () => T, dependencies?: any[]): T;
export function useSignalState<T>(getter: () => T, dependencies?: any[]): T;
/**
* A hook that subscribes to changes in the given atom or selector. Returns
* a binding that updates when the state changes.
* Returns a React binding that updates when the getter function updates.
*
* If the `dependencies` array is provided, a new effect is created for the
* current `callback` when the dependencies change. Otherwise, the effect is
* current `getter` when the dependencies change. Otherwise, the effect is
* created only once, when the component is mounted.
*
* @param callback The atom or selector to watch.
* @param dependencies An array of values used to memoize the callback.
* @returns A binding that stores the current state.
* @param getter The atom or selector to watch.
* @param dependencies An array of values used to memoize the getter.
* @return A binding that stores the current state.
*/
export function useAtomBinding<T>(callback: () => T, dependencies?: any[]): Binding<T>;
export function useSignalBinding<T>(getter: () => T, dependencies?: any[]): Binding<T>;
// Aliases
export { useSignalState as useAtom };

View file

@ -1,31 +1,30 @@
local Charm = require("./Charm")
local React = require("./React")
--- A hook that subscribes to changes in the given atom or selector. The
--- component is re-rendered with the new state after an update.
--- Returns a React state that updates when the getter function updates.
--- Re-renders the component when the value changes.
---
--- If the `dependencies` array is provided, a new effect is created for the
--- current `getter` when the dependencies change. Otherwise, the effect is
--- created only once, when the component is mounted.
---
--- @param getter The atom or selector to watch.
--- @param getter The signal or selector to watch.
--- @param dependencies An array of values used to memoize the getter.
--- @return The current state.
local function useAtom<T>(getter: () -> T, dependencies: { any }?): T
local state, setState = React.useState(getter)
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(setState, getter())
Charm.untracked(setValue, getter())
end)
end, dependencies or {})
return state
return value
end
--- A hook that subscribes to changes in the given atom or selector. Returns
--- a binding that updates when the state changes.
--- Returns a React binding that updates when the getter function updates.
---
--- If the `dependencies` array is provided, a new effect is created for the
--- current `getter` when the dependencies change. Otherwise, the effect is
@ -34,9 +33,9 @@ end
--- @param getter The atom or selector to watch.
--- @param dependencies An array of values used to memoize the getter.
--- @return A binding that stores the current state.
local function useAtomBinding<T>(getter: () -> T, dependencies: { any }?)
local initialState = React.useMemo(getter, {})
local binding, setBinding = React.useBinding(initialState)
local function useSignalBinding<T>(getter: () -> T, dependencies: { any }?)
local initialValue = React.useMemo(getter, {})
local binding, setBinding = React.useBinding(initialValue)
React.useLayoutEffect(function()
return Charm.effect(function()
@ -49,6 +48,7 @@ local function useAtomBinding<T>(getter: () -> T, dependencies: { any }?)
end
return {
useAtom = useAtom,
useAtomBinding = useAtomBinding,
useSignalState = useSignalState,
useSignalBinding = useSignalBinding,
useAtom = useSignalState,
}

View file

@ -1,7 +1,7 @@
local suite = require("../../../../test/suite")
suite.describe("useAtom", function()
require("./useAtom.spec")
suite.describe("useSignalState", function()
require("./useSignalState.spec")
end)
return {}

View file

@ -5,55 +5,55 @@ local suite = require("../../../../test/suite")
local test = suite.test
test("tracks atom state", function()
local state = Charm.atom(0)
test("tracks signal state", function()
local getState, setState = Charm.signal(0)
local source
local cleanup = Vide.root(function()
source = VideCharm.useAtom(state)
source = VideCharm.useSignalState(getState)
end)
assert(source() == 0, "should return 0")
state(1)
setState(1)
assert(source() == 1, "should return 1")
cleanup()
end)
test("accepts a selector", function()
local state = Charm.atom(0)
local getState, setState = Charm.signal(0)
local source
local cleanup = Vide.root(function()
source = VideCharm.useAtom(function()
return 2 * state()
source = VideCharm.useSignalState(function()
return 2 * getState()
end)
end)
assert(source() == 0, "should return 0")
state(1)
setState(1)
assert(source() == 2, "should return 2")
cleanup()
end)
test("updates source in untracked", function()
local outerAtom = Charm.atom(nil :: any)
local innerAtom = Charm.atom(0)
local getOuter, setOuter = Charm.signal(nil :: any)
local getInner, setInner = Charm.signal(0)
local spyCalls = 0
local cleanup = Vide.root(function()
return Vide.show(VideCharm.useAtom(outerAtom), function()
-- Charm effects created in Vide scopes triggered by useAtom might
-- incorrectly get treated as an inner effect in useAtom. This test
return Vide.show(VideCharm.useSignalState(getOuter), function()
-- Charm effects created in Vide scopes triggered by useSignalState might
-- incorrectly get treated as an inner effect in useSignalState. This test
-- ensures that doesn't happen.
Vide.cleanup(Charm.effect(function()
innerAtom()
getInner()
spyCalls += 1
end))
return nil
end)
end)
assert(spyCalls == 0, "should not call spy initially")
outerAtom(true)
setOuter(true)
assert(spyCalls == 1, "should call spy when visible")
innerAtom(1)
setInner(1)
assert(spyCalls == 2, "spy should run when counter updates")
outerAtom({}) -- set to a truthy value to trigger an outer effect re-run
innerAtom(2)
setOuter({}) -- set to a truthy value to trigger an outer effect re-run
setInner(2)
assert(spyCalls == 3, "spy should still run when counter updates")
cleanup()
end)

View file

@ -1,8 +1,11 @@
/**
* Subscribes to the state of an atom or selector. Returns a source that
* updates when the state changes.
* Returns a reactive source that subscribes to the current value of the given
* signal or getter function.
*
* @param callback The atom or selector to subscribe to.
* @returns The reactive source.
* @param getter The getter function to subscribe to.
* @return The reactive source.
*/
export function useAtom<T>(callback: () => T, dependencies?: any[]): () => T;
export function useSignalState<T>(getter: () => T, dependencies?: any[]): () => T;
// Aliases
export { useSignalState as useAtom };

View file

@ -1,12 +1,12 @@
local Charm = require("./Charm")
local Vide = require("./Vide")
--- Returns a reactive source that updates when the given getter function's
--- value changes.
--- Returns a reactive source that subscribes to the current value of the given
--- signal or getter function.
---
--- @param getter The atom or selector to subscribe to.
--- @param getter The getter function to subscribe to.
--- @return The reactive source.
local function useAtom<State>(getter: () -> State): () -> State
local function useSignalState<State>(getter: () -> State): () -> State
local state = Vide.source(getter())
Vide.cleanup(Charm.effect(function()
@ -18,5 +18,6 @@ local function useAtom<State>(getter: () -> State): () -> State
end
return {
useAtom = useAtom,
useSignalState = useSignalState,
useAtom = useSignalState,
}