reactjs-signal 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/commit-convention.md +95 -0
- package/.github/workflows/main.yml +21 -0
- package/.github/workflows/release.yml +29 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/package.json +44 -0
- package/src/index.ts +450 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +12 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
## Git Commit Message Convention
|
|
2
|
+
|
|
3
|
+
> This is adapted from [Commit convention](https://www.conventionalcommits.org/en/v1.0.0/).
|
|
4
|
+
|
|
5
|
+
#### TL;DR:
|
|
6
|
+
|
|
7
|
+
Messages must be matched by the following regex:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
/^((feat|fix|docs|style|core|i18n|report|misc|cli|audits|refactor|perf|test|workflow|build|ci|chore|types|wip|release|deps?|merge|examples?|revert)(\(.+\))?(\:|\!\:)|(Merge|Revert|Version)) .{1,50}$/;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
#### Examples
|
|
14
|
+
|
|
15
|
+
Appears under "Features" header, `compiler` subheader:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
feat(compiler): add 'comments' option
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Appears under "Bug Fixes" header, `v-model` subheader, with a link to issue #28:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
fix(v-model): handle events on blur
|
|
25
|
+
|
|
26
|
+
close #28
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
perf(core): improve vdom diffing by removing 'foo' option
|
|
33
|
+
|
|
34
|
+
BREAKING CHANGE: The 'foo' option has been removed.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
revert: feat(compiler): add 'comments' option
|
|
41
|
+
|
|
42
|
+
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Full Message Format
|
|
46
|
+
|
|
47
|
+
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
<type>(<scope>): <subject>
|
|
51
|
+
<BLANK LINE>
|
|
52
|
+
<body>
|
|
53
|
+
<BLANK LINE>
|
|
54
|
+
<footer>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The **header** is mandatory and the **scope** of the header is optional.
|
|
58
|
+
|
|
59
|
+
### Revert
|
|
60
|
+
|
|
61
|
+
If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
|
|
62
|
+
|
|
63
|
+
### Type
|
|
64
|
+
|
|
65
|
+
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
|
|
66
|
+
|
|
67
|
+
Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
|
|
68
|
+
|
|
69
|
+
### Scope
|
|
70
|
+
|
|
71
|
+
The scope could be anything specifying the place of the commit change. For example `core`, `compiler`, `ssr`, `v-model`, `transition` etc...
|
|
72
|
+
|
|
73
|
+
### Subject
|
|
74
|
+
|
|
75
|
+
The subject contains a succinct description of the change:
|
|
76
|
+
|
|
77
|
+
- use the imperative, present tense: "change" not "changed" nor "changes"
|
|
78
|
+
- don't capitalize the first letter
|
|
79
|
+
- no dot (.) at the end
|
|
80
|
+
|
|
81
|
+
### Body
|
|
82
|
+
|
|
83
|
+
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
|
|
84
|
+
The body should include the motivation for the change and contrast this with previous behavior.
|
|
85
|
+
|
|
86
|
+
### Footer
|
|
87
|
+
|
|
88
|
+
The footer should contain any information about **Breaking Changes** and is also the place to
|
|
89
|
+
reference GitHub issues that this commit **Closes**.
|
|
90
|
+
|
|
91
|
+
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
feat!: breaking change / feat(scope)!: rework API
|
|
95
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- master
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
- uses: actions/setup-node@v3
|
|
17
|
+
with:
|
|
18
|
+
node-version: 18.x
|
|
19
|
+
|
|
20
|
+
- run: npm install
|
|
21
|
+
- run: npm run lint && npm run build
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
permissions:
|
|
4
|
+
contents: write
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- 'v*'
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v3
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v3
|
|
20
|
+
with:
|
|
21
|
+
node-version: 18.x
|
|
22
|
+
|
|
23
|
+
- run: npx changeloggithub@latest
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
26
|
+
|
|
27
|
+
- name: Publish
|
|
28
|
+
run: |
|
|
29
|
+
echo "Publishing"
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 opensource
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Template create npm package
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reactjs-signal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"watch": "npm run build -- --watch src",
|
|
17
|
+
"prepublishOnly": "npm run build",
|
|
18
|
+
"lint": "tsc",
|
|
19
|
+
"start": "esno src/index.ts",
|
|
20
|
+
"test": "vitest",
|
|
21
|
+
"verify-commit": "verify-commit-msg",
|
|
22
|
+
"prepare": "git-scm-hooks",
|
|
23
|
+
"release": "bumpp -r && npm publish"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^19.0.2",
|
|
30
|
+
"bumpp": "^9.2.1",
|
|
31
|
+
"git-scm-hooks": "^0.0.7",
|
|
32
|
+
"tsup": "^8.0.1",
|
|
33
|
+
"typescript": "^5.3.3",
|
|
34
|
+
"verify-commit-msg": "^0.0.10"
|
|
35
|
+
},
|
|
36
|
+
"git-hooks": {
|
|
37
|
+
"pre-commit": "npm run lint",
|
|
38
|
+
"commit-msg": "npm run verify-commit"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"alien-signals": ">=0.4",
|
|
42
|
+
"react": ">=18"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* React Alien Signals is a **TypeScript** library that provides hooks built on top of [Alien Signals](https://github.com/stackblitz/alien-signals).
|
|
5
|
+
* It offers a seamless integration with React, ensuring concurrency-safe re-renders without tearing.
|
|
6
|
+
*
|
|
7
|
+
* @module reactjs-signal
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
computed as alienComputed,
|
|
12
|
+
effect as alienEffect,
|
|
13
|
+
effectScope as alienEffectScope,
|
|
14
|
+
signal as alienSignal,
|
|
15
|
+
unstable as alienUnstable,
|
|
16
|
+
type Effect,
|
|
17
|
+
type Computed,
|
|
18
|
+
type Dependency,
|
|
19
|
+
type EffectScope,
|
|
20
|
+
type ISignal,
|
|
21
|
+
type IWritableSignal,
|
|
22
|
+
} from 'alien-signals';
|
|
23
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
24
|
+
|
|
25
|
+
declare class AsyncComputed<T = any> extends Computed {
|
|
26
|
+
get(): Promise<T>;
|
|
27
|
+
//@ts-ignore
|
|
28
|
+
update(): Promise<boolean>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a writable Alien Signal.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const countSignal = createSignal(0);
|
|
37
|
+
* countSignal.set(10); // sets the value to 10
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @template T - The type of the signal value.
|
|
41
|
+
* @param {T} initialValue - The initial value of the signal.
|
|
42
|
+
* @returns {IWritableSignal<T>} The created Alien Signal.
|
|
43
|
+
*/
|
|
44
|
+
export function createSignal<T>(initialValue: T): IWritableSignal<T> {
|
|
45
|
+
return alienSignal<T>(initialValue);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a computed Alien Signal based on a getter function.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const countSignal = createSignal(1);
|
|
54
|
+
* const doubleSignal = createComputed(() => countSignal.get() * 2);
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @template T - The type of the computed value.
|
|
58
|
+
* @param {() => T} fn - A getter function returning a computed value.
|
|
59
|
+
* @returns {ISignal<T>} The created computed signal.
|
|
60
|
+
*/
|
|
61
|
+
export function createComputed<T>(fn: () => T): ISignal<T> {
|
|
62
|
+
return alienComputed<T>(fn);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a side effect in Alien Signals.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const countSignal = createSignal(1);
|
|
71
|
+
* createEffect(() => {
|
|
72
|
+
* console.log('Count is', countSignal.get());
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @template T - The type of the effect value.
|
|
77
|
+
* @param {() => T} fn - A function that will run whenever its tracked signals update.
|
|
78
|
+
* @returns {Effect<T>} The created effect object.
|
|
79
|
+
*/
|
|
80
|
+
export function createEffect<T>(fn: () => T): Effect<T> {
|
|
81
|
+
return alienEffect(fn);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates an Alien Signals effect scope. This scope can manage multiple effects,
|
|
86
|
+
* allowing you to stop or start them together.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const scope = createSignalScope();
|
|
91
|
+
* scope.run(() => {
|
|
92
|
+
* // create effects in here...
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @returns {EffectScope} The created effect scope.
|
|
97
|
+
*/
|
|
98
|
+
export function createSignalScope(): EffectScope {
|
|
99
|
+
return alienEffectScope();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates an async computed signal in Alien Signals. The getter is an async generator
|
|
104
|
+
* that yields dependencies and finally resolves to a computed value.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const asyncComp = createAsyncComputed<number>(async function* () {
|
|
109
|
+
* yield someDependency;
|
|
110
|
+
* return 42;
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @template T - The type of the computed value.
|
|
115
|
+
* @param {() => AsyncGenerator<Dependency, T>} getter - An async generator returning dependencies and ultimately a value.
|
|
116
|
+
* @returns {AsyncComputed<T>} The created async computed signal.
|
|
117
|
+
* @experimental
|
|
118
|
+
*/
|
|
119
|
+
export function unstable_createAsyncComputed<T>(
|
|
120
|
+
getter: () => AsyncGenerator<Dependency, T>,
|
|
121
|
+
): AsyncComputed<T> {
|
|
122
|
+
return alienUnstable.asyncComputed<T>(getter);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates an async effect in Alien Signals. The function is an async generator
|
|
127
|
+
* that yields dependencies as they are discovered.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* createAsyncEffect(async function* () {
|
|
132
|
+
* yield someDependency;
|
|
133
|
+
* console.log('Async effect done!');
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @template T - The type of the effect value.
|
|
138
|
+
* @param {() => AsyncGenerator<Dependency, T>} fn - An async generator returning dependencies.
|
|
139
|
+
* @returns {Promise<T>} The created async effect object.
|
|
140
|
+
*/
|
|
141
|
+
export async function unstable_createAsyncEffect<T>(
|
|
142
|
+
fn: () => AsyncGenerator<Dependency, T>,
|
|
143
|
+
): Promise<T> {
|
|
144
|
+
const eff = alienUnstable.asyncEffect(fn);
|
|
145
|
+
|
|
146
|
+
// Immediately run the effect and return its promise
|
|
147
|
+
const final = await eff.run();
|
|
148
|
+
return final;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Creates a computed array signal in Alien Signals, deriving a reactive
|
|
153
|
+
* array from an original signal array.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* const numbersSignal = createSignal([1, 2, 3]);
|
|
158
|
+
* const compArray = createComputedArray(numbersSignal, (itemSignal, i) => () => {
|
|
159
|
+
* return itemSignal.get() * 2;
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @template I - The type of the items in the input array.
|
|
164
|
+
* @template O - The type of the items in the output array.
|
|
165
|
+
* @param {ISignal<I[]>} arr - Signal containing an array.
|
|
166
|
+
* @param {(itemSignal: ISignal<I>, index: number) => () => O} getGetter - A function returning a getter for each item signal.
|
|
167
|
+
* @returns {Readonly<O[]>} A proxied array signal.
|
|
168
|
+
* @experimental
|
|
169
|
+
*/
|
|
170
|
+
export function unstable_createComputedArray<I, O>(
|
|
171
|
+
arr: ISignal<I[]>,
|
|
172
|
+
getGetter: (itemSignal: ISignal<I>, index: number) => () => O,
|
|
173
|
+
): Readonly<O[]> {
|
|
174
|
+
return alienUnstable.computedArray<I, O>(arr, getGetter);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates a computed Set signal in Alien Signals that tracks changes
|
|
179
|
+
* to a source Set signal.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* const setSignal = createSignal(new Set([1, 2]));
|
|
184
|
+
* const compSet = createComputedSet(setSignal);
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* @template T - The type of the items in the Set.
|
|
188
|
+
* @param {IWritableSignal<Set<T>>} source - A signal containing a Set.
|
|
189
|
+
* @returns {ISignal<Set<T>>} A computed signal referencing that Set.
|
|
190
|
+
* @experimental
|
|
191
|
+
*/
|
|
192
|
+
export function unstable_createComputedSet<T>(source: IWritableSignal<Set<T>>): ISignal<Set<T>> {
|
|
193
|
+
return alienUnstable.computedSet<T>(source);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Creates an equality-based computed signal, only updating when the new value
|
|
198
|
+
* is not deeply equal to the old value.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const eqComp = createEqualityComputed(() => {
|
|
203
|
+
* return { foo: 'bar' };
|
|
204
|
+
* });
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* @template T - The type of the computed value.
|
|
208
|
+
* @param {() => T} getter - A function returning the value to compare.
|
|
209
|
+
* @returns {ISignal<T>} An equality computed signal.
|
|
210
|
+
* @experimental
|
|
211
|
+
*/
|
|
212
|
+
export function unstable_createEqualityComputed<T>(getter: () => T): ISignal<T> {
|
|
213
|
+
return alienUnstable.equalityComputed(getter);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* React hook returning `[value, setValue]` for a given Alien Signal.
|
|
218
|
+
* Uses useSyncExternalStore for concurrency-safe re-renders.
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const countSignal = createSignal(0);
|
|
223
|
+
* function Counter() {
|
|
224
|
+
* const [count, setCount] = useSignal(countSignal);
|
|
225
|
+
* return <button onClick={() => setCount(count + 1)}>{count}</button>;
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @template T - The type of the signal value.
|
|
230
|
+
* @param {IWritableSignal<T>} alienSignal - The signal to read/write.
|
|
231
|
+
* @returns {[T, (val: T | ((oldVal: T) => T)) => void]} A tuple [currentValue, setValue].
|
|
232
|
+
*/
|
|
233
|
+
export function useSignal<T>(
|
|
234
|
+
alienSignal: IWritableSignal<T>,
|
|
235
|
+
): [T, (val: T | ((oldVal: T) => T)) => void] {
|
|
236
|
+
const value = useSyncExternalStore(
|
|
237
|
+
(callback) => {
|
|
238
|
+
const eff = alienEffect(() => {
|
|
239
|
+
alienSignal.get(); // track
|
|
240
|
+
callback();
|
|
241
|
+
});
|
|
242
|
+
return () => eff.stop();
|
|
243
|
+
},
|
|
244
|
+
() => alienSignal.get(),
|
|
245
|
+
() => alienSignal.get(), // server snapshot
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const setValue = (val: T | ((oldVal: T) => T)) => {
|
|
249
|
+
if (typeof val === 'function') {
|
|
250
|
+
alienSignal.set((val as (oldVal: T) => T)(alienSignal.get()));
|
|
251
|
+
} else {
|
|
252
|
+
alienSignal.set(val);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return [value, setValue];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* React hook returning only the current value of an Alien Signal (or computed).
|
|
261
|
+
* No setter is provided.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* const countSignal = createSignal(0);
|
|
266
|
+
* const doubleSignal = createComputed(() => countSignal.get() * 2);
|
|
267
|
+
* function Display() {
|
|
268
|
+
* const count = useSignalValue(countSignal);
|
|
269
|
+
* const double = useSignalValue(doubleSignal);
|
|
270
|
+
* return <div>{count}, {double}</div>;
|
|
271
|
+
* }
|
|
272
|
+
* ```
|
|
273
|
+
*
|
|
274
|
+
* @template T - The type of the signal value.
|
|
275
|
+
* @param {IWritableSignal<T>} alienSignal - The signal to read.
|
|
276
|
+
* @returns {T} The current value.
|
|
277
|
+
*/
|
|
278
|
+
export function useSignalValue<T>(alienSignal: IWritableSignal<T>): T {
|
|
279
|
+
return useSyncExternalStore(
|
|
280
|
+
(callback) => {
|
|
281
|
+
const eff = alienEffect(() => {
|
|
282
|
+
alienSignal.get();
|
|
283
|
+
callback();
|
|
284
|
+
});
|
|
285
|
+
return () => eff.stop();
|
|
286
|
+
},
|
|
287
|
+
() => alienSignal.get(),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* React hook returning only a setter function for an Alien Signal.
|
|
293
|
+
* No current value is provided, similar to Jotai's useSetAtom.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```typescript
|
|
297
|
+
* const countSignal = createSignal(0);
|
|
298
|
+
* function Incrementor() {
|
|
299
|
+
* const setCount = useSetSignal(countSignal);
|
|
300
|
+
* return <button onClick={() => setCount((c) => c + 1)}>+1</button>;
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*
|
|
304
|
+
* @template T - The type of the signal value.
|
|
305
|
+
* @param {IWritableSignal<T>} alienSignal - The signal to write.
|
|
306
|
+
* @returns {(val: T | ((oldVal: T) => T)) => void} A setter function.
|
|
307
|
+
*/
|
|
308
|
+
export function useSetSignal<T>(
|
|
309
|
+
alienSignal: IWritableSignal<T>,
|
|
310
|
+
): (val: T | ((oldVal: T) => T)) => void {
|
|
311
|
+
return (val) => {
|
|
312
|
+
if (typeof val === 'function') {
|
|
313
|
+
alienSignal.set((val as (oldVal: T) => T)(alienSignal.get()));
|
|
314
|
+
} else {
|
|
315
|
+
alienSignal.set(val);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* React hook for running a side effect whenever Alien Signals' dependencies
|
|
322
|
+
* used in `fn` change. The effect is cleaned up on component unmount.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```typescript
|
|
326
|
+
* function Logger() {
|
|
327
|
+
* useSignalEffect(() => {
|
|
328
|
+
* console.log('Signal changed:', someSignal.get());
|
|
329
|
+
* });
|
|
330
|
+
* return null;
|
|
331
|
+
* }
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @param {() => void} fn - The effect function to run.
|
|
335
|
+
*/
|
|
336
|
+
export function useSignalEffect(fn: () => void): void {
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const eff = alienEffect(fn);
|
|
339
|
+
return () => eff.stop();
|
|
340
|
+
}, [fn]);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* React hook for managing an Alien Signals effect scope.
|
|
345
|
+
* All signals/effects created inside this scope run when the component mounts,
|
|
346
|
+
* and are stopped automatically when the component unmounts.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* function ScopedEffects() {
|
|
351
|
+
* const scope = useSignalScope();
|
|
352
|
+
* useEffect(() => {
|
|
353
|
+
* scope.run(() => {
|
|
354
|
+
* createEffect(() => {
|
|
355
|
+
* console.log('Scoped effect:', someSignal.get());
|
|
356
|
+
* });
|
|
357
|
+
* });
|
|
358
|
+
* }, [scope]);
|
|
359
|
+
* return null;
|
|
360
|
+
* }
|
|
361
|
+
* ```
|
|
362
|
+
*
|
|
363
|
+
* @returns {EffectScope} The created effect scope.
|
|
364
|
+
*/
|
|
365
|
+
export function useSignalScope(): EffectScope {
|
|
366
|
+
const scope = useMemo(() => alienEffectScope(), []);
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
return () => {
|
|
369
|
+
scope.stop();
|
|
370
|
+
};
|
|
371
|
+
}, [scope]);
|
|
372
|
+
return scope;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* React hook to read from an async computed signal.
|
|
377
|
+
* The hook fetches the current value, subscribing to changes via useSyncExternalStore,
|
|
378
|
+
* and triggers a get() call to retrieve updated data. Maintains an internal state
|
|
379
|
+
* for the resolved value of the promise.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```typescript
|
|
383
|
+
* const asyncSignal = createAsyncComputed<number>(async function*() {
|
|
384
|
+
* const val = someSignal.get();
|
|
385
|
+
* yield Promise.resolve(someSignal); // track async dep
|
|
386
|
+
* return val * 2;
|
|
387
|
+
* });
|
|
388
|
+
*
|
|
389
|
+
* function AsyncDisplay() {
|
|
390
|
+
* const value = useAsyncComputedValue(asyncSignal);
|
|
391
|
+
* return <div>Value: {String(value)}</div>;
|
|
392
|
+
* }
|
|
393
|
+
* ```
|
|
394
|
+
*
|
|
395
|
+
* @template T - The type of the computed value.
|
|
396
|
+
* @param {AsyncComputed<T>} alienAsyncComp - The async computed signal to read.
|
|
397
|
+
* @returns {T | undefined} The resolved value (or undefined if not yet resolved).
|
|
398
|
+
* @experimental
|
|
399
|
+
*/
|
|
400
|
+
export function unstable_useAsyncComputedValue<T>(alienAsyncComp: AsyncComputed<T>): T | undefined {
|
|
401
|
+
const [value, setValue] = useState<T | undefined>(alienAsyncComp.currentValue);
|
|
402
|
+
|
|
403
|
+
useSyncExternalStore(
|
|
404
|
+
(callback) => {
|
|
405
|
+
const eff = alienEffect(() => {
|
|
406
|
+
alienAsyncComp.currentValue; // track
|
|
407
|
+
callback();
|
|
408
|
+
});
|
|
409
|
+
return () => eff.stop();
|
|
410
|
+
},
|
|
411
|
+
() => alienAsyncComp.currentValue,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
let active = true;
|
|
416
|
+
const fetchValue = async () => {
|
|
417
|
+
const val = await alienAsyncComp.get();
|
|
418
|
+
if (active) setValue(val);
|
|
419
|
+
};
|
|
420
|
+
fetchValue();
|
|
421
|
+
return () => {
|
|
422
|
+
active = false;
|
|
423
|
+
};
|
|
424
|
+
}, [alienAsyncComp]);
|
|
425
|
+
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* React hook to run an asynchronous effect whenever the component mounts,
|
|
431
|
+
* cleaning up when it unmounts.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```typescript
|
|
435
|
+
* useAsyncEffect(async function* () {
|
|
436
|
+
* yield someDependency;
|
|
437
|
+
* console.log('Async side effect complete!');
|
|
438
|
+
* });
|
|
439
|
+
* ```
|
|
440
|
+
*
|
|
441
|
+
* @template T - The type of the effect value.
|
|
442
|
+
* @param {() => AsyncGenerator<Dependency, T>} fn - An async generator representing the effect logic.
|
|
443
|
+
* @experimental
|
|
444
|
+
*/
|
|
445
|
+
export function unstable_useAsyncEffect<T>(fn: () => AsyncGenerator<Dependency, T>): void {
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
const eff = alienUnstable.asyncEffect(fn);
|
|
448
|
+
return () => eff.stop();
|
|
449
|
+
}, [fn]);
|
|
450
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Enable latest features
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
|
|
10
|
+
// Bundler mode
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
// Best practices
|
|
17
|
+
"strict": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
|
|
21
|
+
// Some stricter flags (disabled by default)
|
|
22
|
+
"noUnusedLocals": false,
|
|
23
|
+
"noUnusedParameters": false,
|
|
24
|
+
"noPropertyAccessFromIndexSignature": false
|
|
25
|
+
}
|
|
26
|
+
}
|
package/tsup.config.ts
ADDED