mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 12:41:37 +00:00
feat!: run listen and subscribe callback in untracked
This commit is contained in:
parent
50afcaf6e9
commit
5c27052965
11 changed files with 28 additions and 156 deletions
42
README.md
42
README.md
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Charm is a state management library that brings the concept of [fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) to Roblox. Connect behavior to data with reactive signals, ensuring games stay up-to-date with their underlying data while eliminating the need for manual updates.
|
Charm is a state management library based on [fine-grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf). Connect behavior to data with reactive signals, ensuring games stay up-to-date with their underlying data while eliminating the need for manual updates.
|
||||||
|
|
||||||
**Build game state from simple building blocks:**
|
**Build game state from simple building blocks:**
|
||||||
|
|
||||||
|
|
@ -230,7 +230,7 @@ dispose() -- Cleanup 1
|
||||||
|
|
||||||
### Nested Effects
|
### Nested Effects
|
||||||
|
|
||||||
Effects can be nested inside other effects. When the outer effect re-runs, inner effects from the previous run are automatically cleaned up, and new inner effects are created if needed. The system ensures proper execution order; outer effects always run before their inner effects:
|
An effect is _nested_ if it was created during the execution of another effect. In Charm, when an effect with nested effects re-runs or gets cleaned up, the nested effects from the previous run are automatically cleaned up and re-created if needed. This prevents memory leaks and ensures that outer effects always run before their inner effects:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getPrintCount, setPrintCount = signal(true)
|
local getPrintCount, setPrintCount = signal(true)
|
||||||
|
|
@ -253,8 +253,6 @@ setPrintCount(false)
|
||||||
setCount(3) -- No output
|
setCount(3) -- No output
|
||||||
```
|
```
|
||||||
|
|
||||||
Nested effect cleanup also applies to every reaction API (`subscribe()`, `listen()`, `observe()`) because they are built upon effects.
|
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> To run code that is "detached" from the parent effect or scope, use `untracked()` or a detached effect scope. If you suspect that the new nested effect behavior is causing issues with migration, try disabling the `flags.trackInnerEffects` flag to assist with debugging.
|
> To run code that is "detached" from the parent effect or scope, use `untracked()` or a detached effect scope. If you suspect that the new nested effect behavior is causing issues with migration, try disabling the `flags.trackInnerEffects` flag to assist with debugging.
|
||||||
|
|
||||||
|
|
@ -332,11 +330,11 @@ end) -- Count is 0 (was nil)
|
||||||
setCounter(1) -- Count is 1 (was 0)
|
setCounter(1) -- Count is 1 (was 0)
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that nested effects can be created inside `listen`, and they will clean up automatically when the listener re-runs or gets disposed.
|
Note that the listener callback runs in `untracked()`, so nested effects are not cleaned up.
|
||||||
|
|
||||||
### Getter functions
|
### Getter functions
|
||||||
|
|
||||||
Reaction APIs like `listen()`, `subscribe()`, and `observe()` aren't only restricted to signals. You can listen to "getter" functions that call one or more signals, and they will automatically be memoized:
|
In most Charm APIs, you can also subscribe to getter functions that call one or more signals, and they will automatically be tracked:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getCounter, setCounter = signal(0)
|
local getCounter, setCounter = signal(0)
|
||||||
|
|
@ -375,7 +373,7 @@ setItems({ a = 0, c = 0 }) -- Removed b, Added c
|
||||||
The callback runs in an effect scope, so effects created in the callback will be disposed when the key is removed:
|
The callback runs in an effect scope, so effects created in the callback will be disposed when the key is removed:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getItems, setItems = signal({ a = 0, b = 0 })
|
local getItems, setItems = signal({})
|
||||||
|
|
||||||
local dispose = observe(getItems, function(value, key)
|
local dispose = observe(getItems, function(value, key)
|
||||||
local getValue = computed(function(prevValue)
|
local getValue = computed(function(prevValue)
|
||||||
|
|
@ -384,14 +382,15 @@ local dispose = observe(getItems, function(value, key)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
local value = getValue()
|
local value = getValue()
|
||||||
print(`{key} = {value}`)
|
print(`Set {key} = {value}`)
|
||||||
return function()
|
return function()
|
||||||
print(`Cleanup {key} = {value}`)
|
print(`Cleanup {key} = {value}`)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setItems({ a = 1, b = 0 }) -- Cleanup a = 0, a = 1
|
setItems({ a = 0, b = 0 }) -- Set a = 0, Set b = 0
|
||||||
|
setItems({ a = 1, b = 0 }) -- Cleanup a = 0, Set a = 1
|
||||||
setItems({ a = 1 }) -- Cleanup b = 0
|
setItems({ a = 1 }) -- Cleanup b = 0
|
||||||
dispose() -- Cleanup a = 1
|
dispose() -- Cleanup a = 1
|
||||||
```
|
```
|
||||||
|
|
@ -400,7 +399,7 @@ dispose() -- Cleanup a = 1
|
||||||
|
|
||||||
### `subscribe(getter, callback)`
|
### `subscribe(getter, callback)`
|
||||||
|
|
||||||
The `subscribe` function is identical to `listen()`, but the effect callback skips the initial run. The callback only runs when the value returned by the getter function changes.
|
The `subscribe` function is identical to `listen()`, but the callback does not run initially. The callback only runs when the value returned by the getter function changes.
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getCounter, setCounter = signal(0)
|
local getCounter, setCounter = signal(0)
|
||||||
|
|
@ -974,23 +973,15 @@ print(isReactive(raw)) -- false
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that might help you migrate from an older version.
|
Charm v0.11 introduces several breaking changes, so this section should help you migrate from an older version.
|
||||||
|
|
||||||
<details>
|
For reference, a signal is a state container with a separated getter and setter function. Atoms are a signal with a single function for getting and setting the state.
|
||||||
<summary><b>Terminology</b></summary>
|
|
||||||
|
|
||||||
- Signal: A state container with one function to get the state, and another to update it
|
|
||||||
- Atom: A signal with the getter and setter combined into one function
|
|
||||||
- Effect: Main way to react to state changes
|
|
||||||
- `subscribe(getter, callback)`: Creates an effect that only subscribes to signals accessed in the getter
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**What to look out for:**
|
**What to look out for:**
|
||||||
|
|
||||||
1. Address all of the type errors introduced in your project after updating Charm. Most of them are caused by changes like:
|
1. Address all of the type errors introduced in your project after updating Charm. Most of them are caused by changes like:
|
||||||
- The second arguments of `atom()` changed from an `options` table to an equality function
|
- The second arguments of `atom()` changed from an `options` table to an equality function
|
||||||
- Removed the second argument of `computed()` (you can do your own equality checks now)
|
- Removed the second argument of `computed()` (you can do your own equality checks since the computed callback now receives the previous value)
|
||||||
- Removed the cleanup argument in effect callbacks (`effect(function(cleanup) end)`)
|
- Removed the cleanup argument in effect callbacks (`effect(function(cleanup) end)`)
|
||||||
|
|
||||||
2. If you use Charm Sync, you'll have to rewrite a lot of your sync code. Fortunately, most of the changes should make your code _less_ complicated:
|
2. If you use Charm Sync, you'll have to rewrite a lot of your sync code. Fortunately, most of the changes should make your code _less_ complicated:
|
||||||
|
|
@ -1003,14 +994,9 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
||||||
- `frozen`: Tables passed to signals are deeply frozen to strictly enforce data immutability and prevent accidental mutations
|
- `frozen`: Tables passed to signals are deeply frozen to strictly enforce data immutability and prevent accidental mutations
|
||||||
|
|
||||||
4. Nested effects automatically clean up when the parent effect re-runs or gets disposed. In other words, all effects created during the execution of another effect will be added as a "child" and clean up with the parent effect. This might cause issues in code that relied on the old behavior, where effects were detached from the parent.
|
4. Nested effects automatically clean up when the parent effect re-runs or gets disposed. In other words, all effects created during the execution of another effect will be added as a "child" and clean up with the parent effect. This might cause issues in code that relied on the old behavior, where effects were detached from the parent.
|
||||||
- This feature applies to all reaction APIs, including the listener function in `subscribe()` and the observer function in `observe()`.
|
- This feature applies to the observer function in `observe()`, which also automatically cleans up inner effects to prevent memory leaks. However, this does not apply to `subscribe()` or `listen()`.
|
||||||
- Effects that should not be tracked by a parent effect/scope should be wrapped in [`untracked()`](#untrackedcallback).
|
- Effects that should not be tracked by a parent effect/scope should be wrapped in [`untracked()`](#untrackedcallback).
|
||||||
- This feature can introduce runtime bugs in migrated code. If you suspect this to be the cause, to help identify the issue, you can disable this feature by setting [`flags.trackInnerEffects`](#flags) to `false`.
|
- This feature can introduce runtime bugs in migrated code. If you suspect this to be the cause, to help identify the issue, you can temporarily disable this feature by setting [`flags.trackInnerEffects`](#flags) to `false`.
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> An example of nested effects causing a bug is an old implementation of [`VideCharm.useAtom`](./packages/vide-charm/src/init.luau) that did not wrap the source update in `untracked()`. Because Vide effects run immediately after a source updates, Vide will notify components during the execution of the Charm effect in `useAtom`.
|
|
||||||
>
|
|
||||||
> This meant effects created as a side effect of a source update would implicitly get added as a child of the `useAtom` effect, and they could get disposed at the wrong time and desync UI.
|
|
||||||
|
|
||||||
5. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
|
5. Consider refactoring your code to use some new quality-of-life features. Many of these are made possible thanks to [alien-signals](https://github.com/stackblitz/alien-signals)!
|
||||||
- [`signal()`](#signalinitialvalue-equals): make atom reads and writes more explicit
|
- [`signal()`](#signalinitialvalue-equals): make atom reads and writes more explicit
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ end
|
||||||
|
|
||||||
local Charm = require(script.Parent.Parent.charm.src)
|
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
|
return Charm
|
||||||
|
|
|
||||||
|
|
@ -809,7 +809,7 @@ local function listen<T>(getter: Getter<T>, fn: (state: T, lastState: T?) -> ())
|
||||||
return effect(function()
|
return effect(function()
|
||||||
local previous = current
|
local previous = current
|
||||||
current = tracked()
|
current = tracked()
|
||||||
peek(fn, current, previous)
|
untracked(fn, current, previous)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -837,7 +837,7 @@ local function subscribe<T>(getter: Getter<T>, fn: (state: T, lastState: T) -> (
|
||||||
if firstRun then
|
if firstRun then
|
||||||
firstRun = false
|
firstRun = false
|
||||||
else
|
else
|
||||||
peek(fn, current, previous)
|
untracked(fn, current, previous)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ local test = require("../../../tests/test")
|
||||||
|
|
||||||
local signal = Charm.signal
|
local signal = Charm.signal
|
||||||
local listen = Charm.listen
|
local listen = Charm.listen
|
||||||
local effect = Charm.effect
|
|
||||||
|
|
||||||
test("should listen to selector", function()
|
test("should listen to selector", function()
|
||||||
local source, setSource = signal(0)
|
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")
|
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run in peek", function()
|
test("should run in untracked", function()
|
||||||
local runs = 0
|
local runs = 0
|
||||||
local cleanups = 0
|
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
local b, setB = signal(0)
|
local b, setB = signal(0)
|
||||||
|
|
||||||
listen(a, function()
|
listen(a, function()
|
||||||
runs += 1
|
runs += 1
|
||||||
b()
|
b()
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
cleanups += 1
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(runs == 1, "should run once on init")
|
assert(runs == 1, "should run once on init")
|
||||||
assert(cleanups == 0, "should not cleanup yet")
|
|
||||||
|
|
||||||
setA(1)
|
setA(1)
|
||||||
assert(runs == 2, "should run on a change")
|
assert(runs == 2, "should run once on change")
|
||||||
assert(cleanups == 1, "should cleanup first call")
|
|
||||||
|
|
||||||
setB(1)
|
setB(1)
|
||||||
assert(runs == 2, "should not run on b change")
|
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)
|
end)
|
||||||
|
|
||||||
test("should run if first value is nil", function()
|
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")
|
assert(runs == 2, "should run once on change")
|
||||||
end)
|
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()
|
test("should skip redundant updates", function()
|
||||||
local order = {}
|
local order = {}
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ local test = require("../../../tests/test")
|
||||||
local signal = Charm.signal
|
local signal = Charm.signal
|
||||||
local computed = Charm.computed
|
local computed = Charm.computed
|
||||||
local subscribe = Charm.subscribe
|
local subscribe = Charm.subscribe
|
||||||
local listen = Charm.listen
|
|
||||||
local effect = Charm.effect
|
|
||||||
|
|
||||||
test("should subscribe to selector", function()
|
test("should subscribe to selector", function()
|
||||||
local source, setSource = signal(0)
|
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")
|
assert(current == 2 and previous == 1, "should not run after unsub")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should run in peek", function()
|
test("should run in untracked", function()
|
||||||
local runs = 0
|
local runs = 0
|
||||||
local cleanups = 0
|
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
local b, setB = signal(0)
|
local b, setB = signal(0)
|
||||||
|
|
||||||
subscribe(a, function()
|
subscribe(a, function()
|
||||||
runs += 1
|
runs += 1
|
||||||
b()
|
b()
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
cleanups += 1
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert(runs == 0, "should not run once on init")
|
assert(runs == 0, "should not run once on init")
|
||||||
assert(cleanups == 0, "should not cleanup yet")
|
|
||||||
|
|
||||||
setA(1)
|
setA(1)
|
||||||
assert(runs == 1, "should run once on change")
|
assert(runs == 1, "should run once on change")
|
||||||
assert(cleanups == 0, "should not cleanup yet")
|
|
||||||
|
|
||||||
setB(1)
|
setB(1)
|
||||||
assert(runs == 1, "should not run on b change")
|
assert(runs == 1, "should not run on b change")
|
||||||
assert(cleanups == 0, "should not cleanup first call")
|
|
||||||
|
|
||||||
setA(2)
|
setA(2)
|
||||||
assert(runs == 2, "should run on a change")
|
assert(runs == 2, "should run on a change")
|
||||||
assert(cleanups == 1, "should cleanup first call")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
test("should ensure correct execution order", function()
|
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)}`)
|
assert(table.concat(order) == "123", `bad order: {table.concat(order)}`)
|
||||||
end)
|
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()
|
test("should skip redundant updates", function()
|
||||||
local order = {}
|
local order = {}
|
||||||
local a, setA = signal(0)
|
local a, setA = signal(0)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,4 @@ if not script then
|
||||||
return require("../charm/src")
|
return require("../charm/src")
|
||||||
end
|
end
|
||||||
|
|
||||||
local Charm = require(script.Parent.Parent.charm.src)
|
return require(script.Parent.Parent.charm.src)
|
||||||
|
|
||||||
export type Atom<T = any> = Charm.Atom<T>
|
|
||||||
|
|
||||||
return Charm
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
local Charm = require(script.Parent.Parent.charm.src)
|
return require(script.Parent.Parent.charm.src)
|
||||||
|
|
||||||
export type Atom<T = any> = Charm.Atom<T>
|
|
||||||
|
|
||||||
return Charm
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").React)
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@ local function useSignalState<T>(getter: () -> T, dependencies: { any }?): T
|
||||||
local value, setValue = React.useState(getter)
|
local value, setValue = React.useState(getter)
|
||||||
|
|
||||||
React.useLayoutEffect(function()
|
React.useLayoutEffect(function()
|
||||||
return Charm.effect(function()
|
return Charm.listen(getter, setValue)
|
||||||
-- Wrap in untracked to avoid tracking inner effect and atom calls
|
|
||||||
Charm.untracked(setValue, getter())
|
|
||||||
end)
|
|
||||||
end, dependencies or {})
|
end, dependencies or {})
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
@ -42,10 +39,7 @@ local function useSignalBinding<T>(getter: () -> T, dependencies: { any }?)
|
||||||
local binding, setBinding = React.useBinding(initialValue)
|
local binding, setBinding = React.useBinding(initialValue)
|
||||||
|
|
||||||
React.useLayoutEffect(function()
|
React.useLayoutEffect(function()
|
||||||
return Charm.effect(function()
|
return Charm.listen(getter, setBinding)
|
||||||
-- Wrap in untracked to avoid tracking inner effect and atom calls
|
|
||||||
Charm.untracked(setBinding, getter())
|
|
||||||
end)
|
|
||||||
end, dependencies or {})
|
end, dependencies or {})
|
||||||
|
|
||||||
return binding
|
return binding
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1 @@
|
||||||
local Charm = require(script.Parent.Parent.charm.src)
|
return require(script.Parent.Parent.charm.src)
|
||||||
|
|
||||||
export type Atom<T = any> = Charm.Atom<T>
|
|
||||||
|
|
||||||
return Charm
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,7 @@ local Vide = require("./Vide")
|
||||||
local function useSignalState<State>(getter: () -> State): () -> State
|
local function useSignalState<State>(getter: () -> State): () -> State
|
||||||
local state = Vide.source(getter())
|
local state = Vide.source(getter())
|
||||||
|
|
||||||
Vide.cleanup(Charm.effect(function()
|
Vide.cleanup(Charm.listen(getter, state))
|
||||||
-- Wrap in untracked to avoid tracking inner effect and atom calls
|
|
||||||
Charm.untracked(state :: (State) -> State, getter())
|
|
||||||
end))
|
|
||||||
|
|
||||||
return state
|
return state
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue