mirror of
https://github.com/littensy/charm.git
synced 2026-10-22 21:01:38 +00:00
feat!: use alien-signals for reactivity (#50)
* feat: use alien-signals * refactor: remove pnpm, fix tests * fix: `effectScope` should set scope * fix: check previous value before calling listener * refactor: improve imports * refactor: exclude re-exports from wally * refactor: embrace scope cleanup, restructure code * fix: update pkgs dir references * ci: simplify pesde install * build: install dev deps with wally * feat: add detached, add more tests * test: simplify mapped test update * chore: rephrase doc comment * perf: optimize subscribe and mapped * refactor: avoid ambiguous optional function type * refactor: add assertion error messages * perf: optimize mapped, add benches * chore: delete redundant benchmarks * perf: simplify mapped logic * test: prettify test library * test: try different gray * test: color brackets * refactor: sync 2.0.4 * refactor: use setCurrentSub instead of pauseTracking * test: improve test suite * test: separate parent from root * refactor: iterate over previous data in mapped * refactor: improve logic * style: use `---` doc comments * refactor: improve logic * chore: housekeeping * ci: add pnpm step * ci: init submodules * test: use lune * refactor: improve structure and alien-signals integration * ci: update test file path * feat: add peek and roblox-ts types * feat: add deep freeze flag, misc refactors * refactor: reduce type casting, housekeeping * refactor: rewrite untracked, update alien-signals * chore: update eslint, housekeeping * refactor: start charm-sync rewrite * fix: send init payload even if atom did not update * test: add all sync tests * feat: prepare for alpha release * feat: add peek, make untracked escape scope * fix: wrap hooks in untracked, update types * feat: add flag for inner effect tracking * refactor: add docs, remove some modules * refactor: align with alien-signals 3.0 * feat: add signal api * refactor: update charm sync api * feat: update alien-signals * refactor: remove unnecessary features, update docs * feat: finalize sync docs * docs: update sync section * docs: simplify examples * docs: capitalize headers * docs: add migration tips * docs: fix whitespace * docs: rephrase computed arg change * refactor: rename batch, misc fixes & perf updates * refactor: simplify charm sync validation * feat: update alien-signals, add recursion flag * feat: add `recursive()` * fix: allow dispose in observers * feat: add `trigger()`, refactor source * docs: update readme * chore: prepare release candidate * test: make some tests more reliable * docs: fix typo * feat: add `reactive()` * chore: bump versions * docs: reword reactive docs * docs: fix typo * feat: add `toRaw()` * fix: check for nil diffs * chore: bump versions * perf: do not create proxies for iteration * fix: make `toRaw` track all calls * fix: check none before patching mutable tables * chore: bump charm-sync version * docs: add fishing game example project * feat: sync signals that return reactive proxy * fix: refine reactive proxy logic * fix: prevent early reactive gc, alien-signals 3.1.1 * fix: wrap iterated values in `reactive()` * fix: loosen client signal types * chore: bump release candidate * fix: remove unsynced signals immediately * test: use lute for tests * chore: bump versions * ci: set up lute * refactor: improve source readability * ci: upgrade lute * build: loosen workspace version requirements * chore: bump deps * fix: update signal overload for setter (#58) * fix: add missing recursed check * test: add important vue reactivity tests * chore: fix react tests, run stylua * chore: update vide * docs: improve intro and explain getters * docs: adjust ordering by importance * docs: move listen nesting note * build: fix react peer dep for ci * chore: update lockfile * ci: use javascript eslint config * ci: ignore eslint config file * refactor: move test directories * feat: allow recursion in effects by default * release: charm v0.11.0-rc.4 * refactor: remove recursive() * refactor: reorganize repo, create deep-charm * docs: rephrase some sections * feat: export getter and setter types * docs: rephrase intro again * feat!: run listen and subscribe callback in untracked * refactor!: remove peek * docs: note peek in migration guide * release: charm v0.11.0-rc.5 * release: charm-sync v0.4.0-rc.7 * release: react-charm v0.4.0-rc.4 * fix: remove old keys when updating key in `mapped()` * refactor: shorten error traceback, housekeeping * refactor: keep error level in some cases * fix: check if cleanup functions disposed effect * refactor!: run effects in batched * ci: check eslint and prettier * perf: run observe fn in untracked * perf: call mapper as-is in `mapped()` * refactor: error for effect cleanup errors * refactor: use new type solver * ci: disable new solver flag until 0.714 * chore: housekeeping * refactor: improve errors in nonstrict, optimize noyield * chore: housekeeping * refactor: satisfy new solver * refactor: improve error traceback * docs: link roblox signals * chore: housekeeping * perf: remove `clearPending` inline operation * refactor: check type of cleanup * ci: remove selene * fix: always return pending value in signal setter * test: add multi benchmark * release: charm v0.11.0-rc.6 * test: add cleanup order tests * chore: bump deps * docs: add warning for deep-charm and note for `observe()` --------- Co-authored-by: safazi <safazics@gmail.com>
This commit is contained in:
parent
d0b7cff943
commit
573533058f
148 changed files with 9222 additions and 4384 deletions
1
packages/react-charm/Charm.luau
Normal file
1
packages/react-charm/Charm.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require(script.Parent.Parent.charm.src)
|
||||
1
packages/react-charm/React.luau
Normal file
1
packages/react-charm/React.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").React)
|
||||
1
packages/react-charm/ReactRoblox.luau
Normal file
1
packages/react-charm/ReactRoblox.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
return require(script.Parent.Parent.Parent:WaitForChild("@rbxts-js").ReactRoblox)
|
||||
6
packages/react-charm/default.project.json
Normal file
6
packages/react-charm/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "react-charm",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
30
packages/react-charm/package.json
Normal file
30
packages/react-charm/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@rbxts/react-charm",
|
||||
"version": "0.4.0-rc.4",
|
||||
"description": "React bindings for Charm state management",
|
||||
"license": "MIT",
|
||||
"main": "src/init.luau",
|
||||
"types": "src/index.d.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"*.luau",
|
||||
"!wally.toml",
|
||||
"!wally.lock",
|
||||
"!default.project.json",
|
||||
"!test"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/littensy/charm.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cp ../../LICENSE . && cp ../../README.md .",
|
||||
"publish:wally": "wally publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rbxts/charm": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rbxts/react": ">=17.0.0"
|
||||
}
|
||||
}
|
||||
31
packages/react-charm/src/index.d.ts
vendored
Normal file
31
packages/react-charm/src/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Binding } from "@rbxts/react";
|
||||
|
||||
/**
|
||||
* 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 signal or selector to watch.
|
||||
* @param dependencies An array of values used to memoize the getter.
|
||||
* @return The current state.
|
||||
*/
|
||||
export function useSignalState<T>(getter: () => T, dependencies?: any[]): T;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* created only once, when the component is mounted.
|
||||
*
|
||||
* @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 useSignalBinding<T>(getter: () => T, dependencies?: any[]): Binding<T>;
|
||||
|
||||
// Aliases
|
||||
export { useSignalState as useAtom };
|
||||
52
packages/react-charm/src/init.luau
Normal file
52
packages/react-charm/src/init.luau
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
local Charm = require("./Charm")
|
||||
local React = require("./React")
|
||||
|
||||
--[[
|
||||
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 signal or selector to watch.
|
||||
@param dependencies An array of values used to memoize the getter.
|
||||
@return The current state.
|
||||
]]
|
||||
local function useSignalState<T>(getter: () -> T, dependencies: {}?): T
|
||||
local value, setValue = React.useState(getter)
|
||||
|
||||
React.useLayoutEffect(function(): () -> ()
|
||||
return Charm.listen(getter, setValue :: (T, T?) -> ())
|
||||
end, dependencies :: { any } or {})
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
--[[
|
||||
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
|
||||
created only once, when the component is mounted.
|
||||
|
||||
@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 useSignalBinding<T>(getter: () -> T, dependencies: {}?)
|
||||
local initialValue = React.useMemo(getter, {})
|
||||
local binding, setBinding = React.useBinding(initialValue)
|
||||
|
||||
React.useLayoutEffect(function(): () -> ()
|
||||
return Charm.listen(getter, setBinding :: (T, T?) -> ())
|
||||
end, dependencies :: { any } or {})
|
||||
|
||||
return binding
|
||||
end
|
||||
|
||||
return table.freeze({
|
||||
useSignalState = useSignalState,
|
||||
useSignalBinding = useSignalBinding,
|
||||
useAtom = useSignalState,
|
||||
})
|
||||
39
packages/react-charm/test/helpers/renderHook.luau
Normal file
39
packages/react-charm/test/helpers/renderHook.luau
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
local React = require("../../React")
|
||||
local ReactRoblox = require("../../ReactRoblox")
|
||||
|
||||
type RenderedHook<Props, Result> = {
|
||||
result: Result,
|
||||
rerender: (Props?) -> (),
|
||||
unmount: () -> (),
|
||||
}
|
||||
|
||||
local function renderHook<Props, Result>(callback: (Props) -> Result, props: Props?): RenderedHook<Props, Result>
|
||||
local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
|
||||
local hook = {}
|
||||
|
||||
local function Component(props: Props)
|
||||
hook.result = callback(props)
|
||||
return nil
|
||||
end
|
||||
|
||||
function hook.rerender(newProps: Props?)
|
||||
props = newProps or props
|
||||
ReactRoblox.act(function()
|
||||
root:render(React.createElement(Component, props) :: any)
|
||||
end)
|
||||
end
|
||||
|
||||
function hook.unmount()
|
||||
ReactRoblox.act(function()
|
||||
root:unmount()
|
||||
end)
|
||||
end
|
||||
|
||||
ReactRoblox.act(function()
|
||||
hook.rerender(props)
|
||||
end)
|
||||
|
||||
return hook
|
||||
end
|
||||
|
||||
return renderHook
|
||||
13
packages/react-charm/test/init.luau
Normal file
13
packages/react-charm/test/init.luau
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
local test = require("../../tests/test")
|
||||
|
||||
_G.__ROACT_17_MOCK_SCHEDULER__ = true
|
||||
|
||||
test.describe("useSignalState", function()
|
||||
require("@self/useSignalState.test")
|
||||
end)
|
||||
|
||||
test.describe("useSignalBinding", function()
|
||||
require("@self/useSignalBinding.test")
|
||||
end)
|
||||
|
||||
return {}
|
||||
44
packages/react-charm/test/useSignalBinding.test.luau
Normal file
44
packages/react-charm/test/useSignalBinding.test.luau
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
local Charm = require("../Charm")
|
||||
local ReactCharm = require("../src")
|
||||
local renderHook = require("./helpers/renderHook")
|
||||
local test = require("../../../tests/test")
|
||||
|
||||
test("tracks signal state", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalBinding(getState)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalBinding(function()
|
||||
return 2 * getState()
|
||||
end)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 2, "should return 2")
|
||||
end)
|
||||
|
||||
test("tracks dependencies", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalBinding(getState, { getState })
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
|
||||
getState, setState = Charm.signal(0)
|
||||
hook.rerender()
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
return {}
|
||||
44
packages/react-charm/test/useSignalState.test.luau
Normal file
44
packages/react-charm/test/useSignalState.test.luau
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
local Charm = require("../Charm")
|
||||
local ReactCharm = require("../src")
|
||||
local renderHook = require("./helpers/renderHook")
|
||||
local test = require("../../../tests/test")
|
||||
|
||||
test("tracks signal state", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalState(getState)
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalState(function()
|
||||
return 2 * getState()
|
||||
end)
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result == 2, "should return 2")
|
||||
end)
|
||||
|
||||
test("tracks dependencies", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return ReactCharm.useSignalState(getState, { getState })
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
|
||||
getState, setState = Charm.signal(0)
|
||||
hook.rerender()
|
||||
assert(hook.result == 0, "should return 0")
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
end)
|
||||
|
||||
return {}
|
||||
17
packages/react-charm/wally.toml
Normal file
17
packages/react-charm/wally.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "littensy/react-charm"
|
||||
version = "0.4.0-rc.4"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
exclude = [
|
||||
"node_modules",
|
||||
"test",
|
||||
"src/index.d.ts",
|
||||
"package.json",
|
||||
"React.luau",
|
||||
"Charm.luau",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
Charm = "littensy/charm@^0.11.0-rc.6"
|
||||
React = "jsdotlua/react@^17"
|
||||
Loading…
Add table
Add a link
Reference in a new issue