mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 12:41:37 +00:00
docs: update readme
This commit is contained in:
parent
898fbdba29
commit
29d8add265
1 changed files with 54 additions and 60 deletions
114
README.md
114
README.md
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Charm** is a state management library that aims to bring [reactive signals](https://preactjs.com/blog/signal-boosting/) from libraries like Preact and Solid to Roblox. It's built on the [alien-signals](https://github.com/stackblitz/alien-signals) signal algorithm, expanding upon it further to handle a wide range of use cases, from rendering user interfaces to handling server game logic.
|
**Charm** is a Roblox state management library inspired by the [reactive signals](https://preactjs.com/blog/introducing-signals/) from libraries like Preact and Solid. It's built on [alien-signals](https://github.com/stackblitz/alien-signals), modified for a wide range of use cases: from rendering user interfaces to handling game logic.
|
||||||
|
|
||||||
**Charm works under a few core principles.**
|
**Charm works under a few core principles.**
|
||||||
|
|
||||||
|
|
@ -26,11 +26,11 @@
|
||||||
- React to state updates: reading a signal automatically subscribes to it
|
- React to state updates: reading a signal automatically subscribes to it
|
||||||
- Combine multiple signals: derive new values from existing state that stay up-to-date
|
- Combine multiple signals: derive new values from existing state that stay up-to-date
|
||||||
- Fine-grained reactivity: make targeted updates in response to specific changes in the state
|
- Fine-grained reactivity: make targeted updates in response to specific changes in the state
|
||||||
- Use immutable data: values are compared directly (`==`) to optimize change detection
|
- Data should be immutable: values are compared directly (`==`) to optimize change detection
|
||||||
|
|
||||||
**Want to learn more about signals?**
|
**Want to learn more about signals?**
|
||||||
|
|
||||||
- https://preactjs.com/blog/signal-boosting
|
- https://preactjs.com/blog/introducing-signals
|
||||||
- https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity
|
- https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity
|
||||||
- https://angular.dev/guide/signals
|
- https://angular.dev/guide/signals
|
||||||
|
|
||||||
|
|
@ -52,6 +52,8 @@
|
||||||
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
- [`mapped(getter, mapper)`](#mappedgetter-mapper)
|
||||||
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
- [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)
|
||||||
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
- [`atom(initialValue, equals?)`](#atominitialvalue-equals)
|
||||||
|
- [`trigger(callback)`](#triggercallback)
|
||||||
|
- [`flags`](#flags)
|
||||||
- [Client-Server Sync](#client-server-sync)
|
- [Client-Server Sync](#client-server-sync)
|
||||||
- [Installation](#installation-1)
|
- [Installation](#installation-1)
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
|
|
@ -159,25 +161,25 @@ Signals are the core of reactivity in Charm. The `signal` function creates a rea
|
||||||
```luau
|
```luau
|
||||||
local getCounter, setCounter = signal(0)
|
local getCounter, setCounter = signal(0)
|
||||||
|
|
||||||
print(getCounter()) --> 0
|
print(getCounter()) -- 0
|
||||||
setCounter(1)
|
setCounter(1)
|
||||||
setCounter(function(count)
|
setCounter(function(count)
|
||||||
return count + 1
|
return count + 1
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
Accessing the signal's value in an effect or computed signal will subscribe to it as a dependency. Changing the value will immediately notify every effect and computed signal that depends on said signal, ensuring all of your state is correct and up-to-date.
|
Accessing the signal's value in an effect or computed signal will subscribe to it as a dependency. Changing the value will immediately notify every effect and computed signal that depends on the signal, ensuring all of your state is correct and up-to-date.
|
||||||
|
|
||||||
You can also pass a custom equality function to only update the signal if the new value is _not_ equal to the current value:
|
You can also pass a custom equality function to only update the signal if the equality function returns `false`:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local getMax, setMax = signal(0, function(current, incoming)
|
local getMax, setMax = signal(0, function(current, incoming)
|
||||||
return incoming <= current
|
return incoming <= current
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setMax(1) --> 1
|
setMax(1) -- 1
|
||||||
setMax(2) --> 2
|
setMax(2) -- 2
|
||||||
setMax(-1) --> 2
|
setMax(-1) -- 2
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
|
|
@ -198,9 +200,9 @@ local getFullName = computed(function()
|
||||||
return `{getName()} {getSurname()}`
|
return `{getName()} {getSurname()}`
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(getFullName()) --> "John Doe"
|
print(getFullName()) -- "John Doe"
|
||||||
setName("Jane")
|
setName("Jane")
|
||||||
print(getFullName()) --> "Jane Doe"
|
print(getFullName()) -- "Jane Doe"
|
||||||
```
|
```
|
||||||
|
|
||||||
The getter function also receives the previous state (or `nil` if running for the first time). You can use this for more complex state:
|
The getter function also receives the previous state (or `nil` if running for the first time). You can use this for more complex state:
|
||||||
|
|
@ -211,9 +213,9 @@ local getMax = computed(function(prevMax)
|
||||||
return math.max(getCounter(), prevMax or 0)
|
return math.max(getCounter(), prevMax or 0)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(getMax()) --> 10
|
print(getMax()) -- 10
|
||||||
setCounter(5)
|
setCounter(5)
|
||||||
print(getMax()) --> 10
|
print(getMax()) -- 10
|
||||||
```
|
```
|
||||||
|
|
||||||
Computed signals are updated lazily, meaning the getter will only re-execute once the computed signal is called _and_ if a dependency updated since the last call.
|
Computed signals are updated lazily, meaning the getter will only re-execute once the computed signal is called _and_ if a dependency updated since the last call.
|
||||||
|
|
@ -229,9 +231,9 @@ local getCounter, setCounter = signal(0)
|
||||||
|
|
||||||
effect(function()
|
effect(function()
|
||||||
print(`Count is {getCounter()}`)
|
print(`Count is {getCounter()}`)
|
||||||
end) --> Count is 0
|
end) -- Count is 0
|
||||||
|
|
||||||
setCounter(1) --> Count is 1
|
setCounter(1) -- Count is 1
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also return a cleanup function that will run once, either before the effect re-runs or when the effect is disposed:
|
You can also return a cleanup function that will run once, either before the effect re-runs or when the effect is disposed:
|
||||||
|
|
@ -246,8 +248,8 @@ local dispose = effect(function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setCounter(1) --> Cleanup 0
|
setCounter(1) -- Cleanup 0
|
||||||
dispose() --> Cleanup 1
|
dispose() -- Cleanup 1
|
||||||
```
|
```
|
||||||
|
|
||||||
Effects can be nested and they will clean up automatically when the parent effect re-runs or gets disposed. This means you don't have to manually clean up inner effects, and this applies to functions like `subscribe()` and `observe()`.
|
Effects can be nested and they will clean up automatically when the parent effect re-runs or gets disposed. This means you don't have to manually clean up inner effects, and this applies to functions like `subscribe()` and `observe()`.
|
||||||
|
|
@ -262,8 +264,8 @@ effect(function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setCounter(1) --> Outer: 1, Inner: 1
|
setCounter(1) -- Outer: 1, Inner: 1
|
||||||
setCounter(2) --> Outer: 2, Inner: 2
|
setCounter(2) -- Outer: 2, Inner: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
|
|
@ -283,29 +285,29 @@ effect(function()
|
||||||
local runs = untracked(getRuns) + 1
|
local runs = untracked(getRuns) + 1
|
||||||
setRuns(runs)
|
setRuns(runs)
|
||||||
print(`Count is {getCounter()}, ran {runs} times`)
|
print(`Count is {getCounter()}, ran {runs} times`)
|
||||||
end) --> Count is 0, ran 1 times
|
end) -- Count is 0, ran 1 time
|
||||||
|
|
||||||
setCounter(10) --> Count is 10, ran 2 times
|
setCounter(10) -- Count is 10, ran 2 times
|
||||||
setRuns(10) -- No output
|
setRuns(10) -- No output
|
||||||
setCounter(20) --> Count is 20, ran 11 times
|
setCounter(20) -- Count is 20, ran 11 times
|
||||||
```
|
```
|
||||||
|
|
||||||
Because `untracked()` executes the callback outside the current effect, nested effects created in the callback do not get tracked by the parent effect:
|
Because `untracked()` executes the callback outside the current effect, nested effects created in the callback do not get tracked by the parent effect:
|
||||||
|
|
||||||
```luau
|
```luau
|
||||||
local disposeInner
|
local stopEffect
|
||||||
local disposeOuter = effect(function()
|
local stopScope = effectScope(function()
|
||||||
untracked(function()
|
untracked(function()
|
||||||
disposeInner = effect(function()
|
stopEffect = effect(function()
|
||||||
return function()
|
return function()
|
||||||
print("Cleaned up inner effect")
|
print("Cleaned up untracked effect")
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
disposeOuter() -- No output; outer effect did not track inner effect
|
stopScope() -- No output, the scope did not track the effect
|
||||||
disposeInner() --> Cleaned up inner effect
|
stopEffect() -- Cleaned up untracked effect
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -318,18 +320,10 @@ Similar to `untracked()`, but does not prevent the parent effect from tracking n
|
||||||
local getCounter, setCounter = signal(0)
|
local getCounter, setCounter = signal(0)
|
||||||
|
|
||||||
local disposeOuter = effect(function()
|
local disposeOuter = effect(function()
|
||||||
peek(function()
|
print(`Count is {peek(getCounter)}`)
|
||||||
print(`Count is {getCounter()}`)
|
end) -- Count is 0
|
||||||
effect(function()
|
|
||||||
return function()
|
|
||||||
print("Cleaned up inner effect")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end) --> Count is 0
|
|
||||||
|
|
||||||
setCounter(1) -- No output; count was accessed in peek()
|
setCounter(1) -- No output; count was accessed in peek()
|
||||||
disposeOuter() --> Cleaned up inner effect
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -372,7 +366,7 @@ local dispose = effectScope(function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setCounter(1) --> Count 1 is 1, Count 2 is 1
|
setCounter(1) -- Count 1 is 1, Count 2 is 1
|
||||||
dispose()
|
dispose()
|
||||||
setCounter(2) -- No output; effects got disposed
|
setCounter(2) -- No output; effects got disposed
|
||||||
```
|
```
|
||||||
|
|
@ -392,9 +386,9 @@ local getCounter, setCounter = signal(0)
|
||||||
|
|
||||||
listen(getCounter, function(count, prevCount)
|
listen(getCounter, function(count, prevCount)
|
||||||
print(`Count is {count} (was {prevCount})`)
|
print(`Count is {count} (was {prevCount})`)
|
||||||
end) --> Count is 0 (was nil)
|
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 nested effects can be created inside `listen`, and they will clean up automatically when the listener re-runs or gets disposed.
|
||||||
|
|
@ -413,7 +407,7 @@ subscribe(getCounter, function(count, prevCount)
|
||||||
print(`Count is {count} (was {prevCount})`)
|
print(`Count is {count} (was {prevCount})`)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setCounter(1) --> Count is 1 (was 0)
|
setCounter(1) -- Count is 1 (was 0)
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that nested effects can be created inside `subscribe`, and they will clean up automatically when the subscription re-runs or gets disposed.
|
Note that nested effects can be created inside `subscribe`, and they will clean up automatically when the subscription re-runs or gets disposed.
|
||||||
|
|
@ -432,9 +426,9 @@ observe(getItems, function(value, key)
|
||||||
return function()
|
return function()
|
||||||
print(`Removed {key}`)
|
print(`Removed {key}`)
|
||||||
end
|
end
|
||||||
end) --> Added a, Added b
|
end) -- Added a, Added b
|
||||||
|
|
||||||
setItems({ a = 0, c = 0 }) --> Removed b, Added c
|
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:
|
||||||
|
|
@ -456,9 +450,9 @@ local dispose = observe(getItems, function(value, key)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
setItems({ a = 1, b = 0 }) --> Cleanup a = 0, a = 1
|
setItems({ a = 1, b = 0 }) -- Cleanup a = 0, a = 1
|
||||||
setItems({ a = 1 }) --> Cleanup b = 0
|
setItems({ a = 1 }) -- Cleanup b = 0
|
||||||
dispose() --> Cleanup a = 1
|
dispose() -- Cleanup a = 1
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -476,7 +470,7 @@ local getUppercase = mapped(getList, function(value)
|
||||||
return string.upper(value)
|
return string.upper(value)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(getUppercase()) --> { "A", "B", "C" }
|
print(getUppercase()) -- { "A", "B", "C" }
|
||||||
```
|
```
|
||||||
|
|
||||||
If the mapper returns two values, the second value is used as the new key:
|
If the mapper returns two values, the second value is used as the new key:
|
||||||
|
|
@ -488,7 +482,7 @@ local getSwapped = mapped(getList, function(value, key)
|
||||||
return key, value
|
return key, value
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(getSwapped()) --> { a = 1, b = 2, c = 3 }
|
print(getSwapped()) -- { a = 1, b = 2, c = 3 }
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -506,7 +500,7 @@ local dispose = effectScope(function()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
dispose() --> Cleaned up
|
dispose() -- Cleaned up
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -520,7 +514,7 @@ If the atom is called with 0 arguments, the atom returns the current value and s
|
||||||
```luau
|
```luau
|
||||||
local counter = atom(0)
|
local counter = atom(0)
|
||||||
|
|
||||||
print(counter()) --> 0
|
print(counter()) -- 0
|
||||||
counter(1)
|
counter(1)
|
||||||
counter(function(count)
|
counter(function(count)
|
||||||
return count + 1
|
return count + 1
|
||||||
|
|
@ -534,8 +528,8 @@ local max = atom(0, function(current, incoming)
|
||||||
return incoming <= current
|
return incoming <= current
|
||||||
end)
|
end)
|
||||||
|
|
||||||
max(1) --> 1
|
max(1) -- 1
|
||||||
max(-1) --> 1
|
max(-1) -- 1
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -553,7 +547,7 @@ effect(function()
|
||||||
setCounter(function(count)
|
setCounter(function(count)
|
||||||
return math.min(count + 1, 3)
|
return math.min(count + 1, 3)
|
||||||
end)
|
end)
|
||||||
end) --> 0, 1, 2, 3
|
end) -- 0, 1, 2, 3
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -568,15 +562,15 @@ local length = computed(function()
|
||||||
return #array()
|
return #array()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(length()) --> 0
|
print(length()) -- 0
|
||||||
|
|
||||||
-- Direct mutation doesn't automatically trigger updates
|
-- Direct mutation doesn't automatically trigger updates
|
||||||
table.insert(array(), 1)
|
table.insert(array(), 1)
|
||||||
print(length()) --> Still 0
|
print(length()) -- Still 0
|
||||||
|
|
||||||
-- Manually trigger updates
|
-- Manually trigger updates
|
||||||
trigger(array)
|
trigger(array)
|
||||||
print(length()) --> 1
|
print(length()) -- 1
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also trigger multiple signals at once:
|
You can also trigger multiple signals at once:
|
||||||
|
|
@ -596,7 +590,7 @@ trigger(function()
|
||||||
src2()
|
src2()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(total()) --> 2
|
print(total()) -- 2
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -884,14 +878,14 @@ Charm v0.11 introduces _a lot_ of breaking changes, so below are some tips that
|
||||||
- Instead of creating client/server syncers, these modules now act like singletons. Sync APIs are called directly through `CharmSync.client`/`server`.
|
- Instead of creating client/server syncers, these modules now act like singletons. Sync APIs are called directly through `CharmSync.client`/`server`.
|
||||||
- [Read the updated docs for syncing state →](#client-server-sync)
|
- [Read the updated docs for syncing state →](#client-server-sync)
|
||||||
|
|
||||||
3. The [`strict` and `frozen` flags](#globals) are automatically enabled in Roblox Studio, so unsafe Charm code will start throwing errors if you didn't use the old `__DEV__` flag. The flags have the following behavior:
|
3. The [`strict` and `frozen` flags](#flags) are automatically enabled in Roblox Studio, so unsafe Charm code will start throwing errors if you didn't use the old `__DEV__` flag. The flags have the following behavior:
|
||||||
- `strict`: Yielding in effects, signals, and other critical Charm functions will throw an error
|
- `strict`: Yielding in effects, signals, and other critical Charm functions will throw an error
|
||||||
- `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 all reaction APIs, including the listener function in `subscribe()` and the observer function in `observe()`.
|
||||||
- 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 [`globals.trackInnerEffects`](#globals) 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 disable this feature by setting [`globals.trackInnerEffects`](#flags) to `false`.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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`.
|
> 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`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue