unplugin-react-fine-grained-signals 0.1.0 → 0.2.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/README.ja.md +266 -42
- package/README.md +267 -42
- package/dist/esbuild.d.ts +3 -3
- package/dist/esbuild.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loader.d.ts +2 -3
- package/dist/rollup.d.ts +3 -3
- package/dist/rollup.js +1 -1
- package/dist/rspack.d.ts +3 -3
- package/dist/rspack.js +1 -1
- package/dist/{unplugin-CG9_mdcA.d.ts → unplugin-B9gaexDa.d.ts} +3 -3
- package/dist/{unplugin-B2_q8KNi.js → unplugin-CPDTNSYq.js} +775 -112
- package/dist/vite.d.ts +14 -5
- package/dist/vite.js +1 -1
- package/dist/webpack.d.ts +3 -3
- package/dist/webpack.js +1 -1
- package/package.json +25 -16
package/README.ja.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
-
[`react-fine-grained-signals`](https://www.npmjs.com/package/react-fine-grained-signals)
|
|
6
|
-
向けの、`
|
|
5
|
+
[`react-fine-grained-signals`](https://www.npmjs.com/package/react-fine-grained-signals)
|
|
6
|
+
向けの、`useSignalTracking()` 自動挿入と任意のmanaged render scope変換を提供する汎用
|
|
7
7
|
bundler integrationです。
|
|
8
8
|
|
|
9
9
|
このpackageは意図的に唯一のbuild-time integrationです。Babel実装は内部に
|
|
@@ -15,8 +15,23 @@ bundler integrationです。
|
|
|
15
15
|
pnpm add -D unplugin-react-fine-grained-signals
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`react-fine-grained-signals` はpeer dependency
|
|
19
|
-
|
|
18
|
+
`react-fine-grained-signals` はpeer dependencyです。Node.jsは
|
|
19
|
+
`^22.18.0 || >=24.11.0` が必要です。変換が依存するBabel 8が宣言している範囲です。
|
|
20
|
+
|
|
21
|
+
pluginとruntimeは同じrelease line(v0.2同士)で使ってください。生成されるコードが
|
|
22
|
+
importするruntimeのentry pointはline間で変わります。v0.1からupgradeする場合は、
|
|
23
|
+
両方のpackageを同時に更新し、`@useSignals` / `@noUseSignals` commentを書き換えて
|
|
24
|
+
ください。[v0.2移行ガイド](https://github.com/sakzw/react-fine-grained-signals/blob/main/docs/migration/v0.2.ja.md#transform-plugin)を参照してください。
|
|
25
|
+
|
|
26
|
+
このpackageはESMだけを配布しており、CommonJS buildはありません。`import` を
|
|
27
|
+
使うESMのbuild設定が基本ですが、CommonJSの設定ファイル(`"type": "module"` の
|
|
28
|
+
ないprojectの `webpack.config.js` や `next.config.js`)からも `require()` で
|
|
29
|
+
読み込めます。上記の範囲のNode.jsはすべて、組み込みの `require(esm)` によって
|
|
30
|
+
ESMのentry pointを読み込むためです。plugin factoryは `default` exportです。
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const signals = require("unplugin-react-fine-grained-signals/webpack").default;
|
|
34
|
+
```
|
|
20
35
|
|
|
21
36
|
## Vite
|
|
22
37
|
|
|
@@ -29,6 +44,68 @@ export default defineConfig({
|
|
|
29
44
|
});
|
|
30
45
|
```
|
|
31
46
|
|
|
47
|
+
### React Compilerと併用する場合
|
|
48
|
+
|
|
49
|
+
React Compilerはこの変換の *後* に実行される必要があります。そうでないと、
|
|
50
|
+
このpluginがcomponentに `"use no memo"` を付ける前に、compilerが `.value` の
|
|
51
|
+
読み取りをmemoizationで消してしまい、そのcomponentは最初のrender以降、無言で
|
|
52
|
+
更新されなくなります。compilerを実行するpluginより前に `signals()` を
|
|
53
|
+
listしてください。
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import react from "@vitejs/plugin-react";
|
|
57
|
+
import { defineConfig } from "vite";
|
|
58
|
+
import signals from "unplugin-react-fine-grained-signals/vite";
|
|
59
|
+
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
plugins: [signals({ mode: "auto" }), react({ compiler: true })],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
検証した構成(Vite 8、`@vitejs/plugin-react` 6.1)では、
|
|
66
|
+
`react({ compiler: true })` にはplugin-reactのoptionalな
|
|
67
|
+
`oxc-transform-react` packageも必要です。`babel-plugin-react-compiler` と
|
|
68
|
+
一緒にinstallしてください。ない場合、buildはplugin-react自身の
|
|
69
|
+
「optionalな `oxc-transform-react` packageが必要」というerrorで止まります。
|
|
70
|
+
|
|
71
|
+
Viteでは、この順序をhookのレベルでも守っています。pluginは `enforce: "pre"`
|
|
72
|
+
で、さらに `transform` hookが `order: "pre"` を宣言しており、Viteは
|
|
73
|
+
`enforce: "pre"` のgroupの中でこの値に基づいて並べ替えます。そのため、
|
|
74
|
+
`react()` を先にlistした場合でも、`@vitejs/plugin-react` 6のcompiler step
|
|
75
|
+
(`react({ compiler: true })`。それ自体は `enforce: "pre"` でhookの `order` は
|
|
76
|
+
なし)より先に実行されます。以前はこの順序だとcomponentが先にcompileされて
|
|
77
|
+
いました。ただしこれはViteの並び順の規則であって、絶対的な保証ではありません。
|
|
78
|
+
transformで同じく `order: "pre"` を宣言し、より前にlistされたpluginは、
|
|
79
|
+
やはり先に実行されます。したがって `signals()` を先頭に置く構成が引き続き
|
|
80
|
+
推奨であり、hookの順序が効かない場合は必須です。Rollupは配列内の位置だけで
|
|
81
|
+
順序を決めますし、Babel経由の構成(`@rolldown/plugin-babel` と
|
|
82
|
+
`reactCompilerPreset()`)はこのpluginとの組み合わせで検証していません。
|
|
83
|
+
|
|
84
|
+
React Compilerとの組み合わせには、次の2つの既知の制約があります。どちらも
|
|
85
|
+
v0.1.1から存在するものです。
|
|
86
|
+
|
|
87
|
+
- **inlineのrender helperがcompilerにmemoizeされることがある。** このplugin
|
|
88
|
+
が呼び出し元のrenderの一部として扱う関数には、独自のboundaryも
|
|
89
|
+
`"use no memo"` も付きません。例は、通常の関数として呼び出すPascalCaseの
|
|
90
|
+
helper(`Title()`)や、`items.map(Row)` のようなrender callbackです。React
|
|
91
|
+
Compilerはそのhelper単体をcompileすることがあります。helperのJSXに、
|
|
92
|
+
compilerから見える依存がない場合(module scopeのsignalの読み取りは依存に
|
|
93
|
+
なりません)、compilerはそのJSXをcacheし、呼び出し元のcomponentが再render
|
|
94
|
+
してもhelperの出力は古いままになります。helperの本体に `"use no memo"` を
|
|
95
|
+
書くか、componentとして(`<Title />`)renderしてください。
|
|
96
|
+
- **作者が書いた `"use memo"` はそのまま残る。** すでにmemoizationの
|
|
97
|
+
directiveを持つ関数には、pluginは `"use no memo"` を追加しません。
|
|
98
|
+
`babel-plugin-react-compiler` 1.0で確認した動作は次のとおりです。
|
|
99
|
+
- managed変換では、compilerは生成された `try` / `finally` をcompileしません。
|
|
100
|
+
既定の `panicThreshold` ではその関数をskipし、
|
|
101
|
+
`panicThreshold: "all_errors"` ではbuildが失敗します("Handle TryStatement
|
|
102
|
+
without a catch clause")。
|
|
103
|
+
- `transform: "inject"` では、compilerがその関数をcompileし、signalの
|
|
104
|
+
読み取りをmemoizationで消してしまうことがあるため、componentが更新され
|
|
105
|
+
なくなることがあります。
|
|
106
|
+
|
|
107
|
+
signalを読む関数では `"use memo"` を使わないでください。
|
|
108
|
+
|
|
32
109
|
## その他のbundler
|
|
33
110
|
|
|
34
111
|
対応するentry pointを同様の要領で使用します。
|
|
@@ -51,21 +128,22 @@ export default {
|
|
|
51
128
|
## オプション
|
|
52
129
|
|
|
53
130
|
- `mode`
|
|
54
|
-
- `"manual"`: 先頭文にあるimport済みの `
|
|
55
|
-
`@
|
|
131
|
+
- `"manual"`: 先頭文にあるimport済みの `useSignalTracking()`、または
|
|
132
|
+
`@signalTracking` を付けた名前付きcomponent/custom hookだけを変換します。
|
|
56
133
|
- `"auto"`(既定): さらに `.value` を読む名前付きJSX componentと
|
|
57
134
|
名前付き `useX` custom hookを変換します。
|
|
58
135
|
- `"all"`: さらにすべての名前付きJSX componentを変換します。ネストした
|
|
59
136
|
callbackの読み取りは親componentが収集しますが、callback自体は変換しません。
|
|
60
137
|
- `transform`
|
|
61
|
-
- `"managed"
|
|
138
|
+
- `"managed"`(既定・推奨): 厳密な `try` / `finally` 境界を追加します。
|
|
62
139
|
`/runtime` からimportし、componentの関数がreturnする時点でrender
|
|
63
140
|
tracking windowを同期的に閉じます。
|
|
64
|
-
- `"inject"
|
|
65
|
-
|
|
66
|
-
として挿入するため、手書きと同じbest-effort
|
|
141
|
+
- `"inject"`(サポートされた高度・互換性向けmode): best-effortなopt-in向けに
|
|
142
|
+
変換なしの `useSignalTracking()` を追加します。制御フローを書き換えず、通常の
|
|
143
|
+
`useSignalTracking()` を先頭hookとして挿入するため、手書きと同じbest-effort
|
|
144
|
+
境界になります。この
|
|
67
145
|
modeが露呈し得るsibling誤帰属の既知の制約については、
|
|
68
|
-
[境界設計の検討docs](../../
|
|
146
|
+
[境界設計の検討docs](../../development/design/use-signals-boundary-design.ja.md)
|
|
69
147
|
を参照してください。
|
|
70
148
|
- `reactCompiler`
|
|
71
149
|
- `"auto"`(既定): 変換したすべての関数に `"use no memo"` を付け、render
|
|
@@ -73,9 +151,11 @@ export default {
|
|
|
73
151
|
消さないようにします。付けない場合、compile済みcomponentはJSXをcacheし、
|
|
74
152
|
最初のsignal書き込み以降、無言で更新が止まります。
|
|
75
153
|
- `"off"`: directiveを付けません。React Compilerをbuildで使っていない場合か、
|
|
76
|
-
対象componentを[互換性の検討docs](../../
|
|
154
|
+
対象componentを[互換性の検討docs](../../development/design/react-compiler-compatibility.ja.md)
|
|
77
155
|
に照らして確認済みの場合だけ選んでください。
|
|
78
156
|
- `importSource`: `react-fine-grained-signals` 互換wrapperへの置き換えです。
|
|
157
|
+
wrapperは、生成されたコードがimportするruntimeのrootの `useSignalTracking` と、
|
|
158
|
+
`/runtime` entryの `useManagedSignals` をre-exportしている必要があります。
|
|
79
159
|
- `reactImportSource`: wrapされた関数をcomponentと判定する際に、`memo` /
|
|
80
160
|
`forwardRef` をReact由来とみなすmodule specifierを追加します。置き換えでは
|
|
81
161
|
なく追加であり、`"react"` からのdirect importは常に認識されます。つまり
|
|
@@ -90,7 +170,10 @@ Rules of Hooksに反するからです。認識するのは、定義箇所にinl
|
|
|
90
170
|
(`items.map((item) => …)`)、変数に切り出してその束縛名で参照して渡す場合
|
|
91
171
|
(`const` の `const Row = …; items.map(Row)` でも、function宣言の
|
|
92
172
|
`function Row() {…}` … `items.map(Row)` でも)、およびそれぞれのoptional
|
|
93
|
-
chaining形(`items?.map(Row)
|
|
173
|
+
chaining形(`items?.map(Row)`)です。`Array.from(arrayLike, mapFn)` の `mapFn`
|
|
174
|
+
(`Array.from({ length: 5 }, Star)`)も同じ扱いで、objectに保持したcomponentを
|
|
175
|
+
分割代入してから渡す場合(`const { Row } = parts; items.map(Row)` や
|
|
176
|
+
`{ Row: Item }`)も同様です。こうしたcallback自体は変換せず、そのJSXと
|
|
94
177
|
`.value` 読み取りは呼び出し元のcomponentが収集します。callbackが同じmodule内の
|
|
95
178
|
別の場所で定義されている場合も同様で、呼び出し元componentの本体自体がsignalを
|
|
96
179
|
読んでいなくても、そのcomponentが変換対象になります。
|
|
@@ -113,7 +196,7 @@ PascalCaseの名前を持っていればよく、`observer(function Row() { …
|
|
|
113
196
|
arrow関数(`observer((props) => …)`)は、boundaryを結び付けるための名前を
|
|
114
197
|
持たないため、変換されないままです。
|
|
115
198
|
|
|
116
|
-
この検出には既知の制約が
|
|
199
|
+
この検出には既知の制約が5つあります。
|
|
117
200
|
|
|
118
201
|
- 再代入したaliasはたどりません。`const RowAlias = Row; items.map(RowAlias)`
|
|
119
202
|
経由で渡したPascalCaseのhelperは、componentとして扱われたままになります。
|
|
@@ -122,12 +205,26 @@ arrow関数(`observer((props) => …)`)は、boundaryを結び付けるた
|
|
|
122
205
|
hook挿入は安全ではありません)でも、`Row` を独立したcomponentとしてinstance化
|
|
123
206
|
する場合(hook挿入こそが更新を成立させます)でも構文が同一で、そのファイルだけ
|
|
124
207
|
では区別できません。そのため `Row` は、単独で変換条件を満たすなら自分のhookを
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
208
|
+
持ったままになります。参照先の関数に `@signalTracking` /
|
|
209
|
+
`@noSignalTracking` コメントで意図を明示してください。
|
|
210
|
+
|
|
211
|
+
callbackをinlineで書くこと
|
|
212
|
+
(`<Grid renderItem={(item) => <li>{item.value}</li>} />`)は、一般的な解決策
|
|
213
|
+
ではありません。その読み取りは呼び出し元componentに帰属するので呼び出し元は
|
|
214
|
+
変換されますが、`Grid` は `renderItem` を *自分の* render中、つまり呼び出し元の
|
|
215
|
+
関数がreturnした後に呼びます。既定のmanaged変換では、その時点で呼び出し元の
|
|
216
|
+
boundaryはすでに閉じているため、`Grid` 自身がboundaryを持つ場合にしか読み取りは
|
|
217
|
+
追跡されず、そうでなければrenderされた項目は古いままになります。`Grid` が
|
|
218
|
+
自分のcomponentなら、`@signalTracking` でopt-inしてください(または `Grid`
|
|
219
|
+
自身でsignalを読んでください)。
|
|
129
220
|
- 別moduleからimportしたcallbackはたどりません。変換は1ファイルずつ処理する
|
|
130
221
|
ためです。
|
|
222
|
+
- objectのkey経由で渡すcallback(`items.map(parts.Row)`、または先に
|
|
223
|
+
`const { Row } = parts` とする場合)はboundaryの対象から外れますが、`auto`
|
|
224
|
+
modeではその `.value` 読み取りが呼び出し元の分として扱われません。呼び出し元
|
|
225
|
+
自身がsignalを読んでいないと、呼び出し元は変換されず更新されません。呼び出し元で
|
|
226
|
+
signalを読むか、`mode: "all"` を使うか、呼び出し元に `@signalTracking` を
|
|
227
|
+
付けてください。
|
|
131
228
|
- 2つの役割を兼ねる関数は、除外されたままになります。`Row` をmodule内のどこかで
|
|
132
229
|
`map` / `flatMap` / `forEach` に渡していて、なおかつ別の場所でJSXタグとして
|
|
133
230
|
単独でrenderしている(`<Row item={x} />`)場合、render callbackとしての用法が
|
|
@@ -135,12 +232,83 @@ arrow関数(`observer((props) => …)`)は、boundaryを結び付けるた
|
|
|
135
232
|
(hookを持たせると、callbackとしての用法でhookの順序が壊れます)。ただし単独で
|
|
136
233
|
renderされるほうはsubscriptionを持たないため、以降のsignalの書き換えで
|
|
137
234
|
内容が古いままになります。役割ごとに別名の2つの関数へ分けるか、単独で
|
|
138
|
-
renderするほうの関数を `@
|
|
235
|
+
renderするほうの関数を `@signalTracking` コメントで明示的にopt-inしてください。
|
|
139
236
|
|
|
140
237
|
判断に迷う場合は、そうしたhelperを明示的に保ってください。小文字で `use`
|
|
141
238
|
始まりでない名前にするか、実際にcomponentとしてrenderされるときにだけ手動で
|
|
142
239
|
opt-inします。
|
|
143
240
|
|
|
241
|
+
## 関数として呼び出されるcomponent
|
|
242
|
+
|
|
243
|
+
module内で通常の関数として呼び出されるPascalCaseの関数 — `Title()`、
|
|
244
|
+
`Title?.()`、`Title!()`、objectに保持されたものなら `ns.Home()`、
|
|
245
|
+
`ns?.group?.Home()`、または兄弟memberからの `this.Home()` — は、Reactが
|
|
246
|
+
独立したcomponentとしてmountすることはなく、その本体は呼び出し元のrenderの
|
|
247
|
+
中でそのまま実行されます。そのため、`all` を含むどのmodeでも独自のboundaryを
|
|
248
|
+
持たず、そのJSXと `.value` 読み取りは、呼び出しを行うcomponentまたはcustom
|
|
249
|
+
hookが収集します。
|
|
250
|
+
|
|
251
|
+
```jsx
|
|
252
|
+
function Title() { return <h1>{count.value}</h1>; }
|
|
253
|
+
// Outerが変換されて `count` をsubscribeし、Titleはそのまま残ります。
|
|
254
|
+
function Outer({ show }) { return <div>{show && Title()}</div>; }
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`Title` の中にboundaryを置くと、`show` がtrueの間だけ実行されるhookが
|
|
258
|
+
`Outer` に加わります。これはReactが拒否する条件付きのhook呼び出しです
|
|
259
|
+
("Rendered more hooks than during the previous render")。event handler、
|
|
260
|
+
`useEffect` / `useLayoutEffect` / `useInsertionEffect` / `useCallback` /
|
|
261
|
+
`useMemo` のcallback、またはasync関数からの呼び出しはrenderの外で実行される
|
|
262
|
+
ため、その読み取りは収集しません。custom hook(`useX`)はもともと関数として
|
|
263
|
+
呼び出すものなので、従来どおり独自のboundaryを持ちます。
|
|
264
|
+
|
|
265
|
+
既知の制約は次のとおりです。
|
|
266
|
+
|
|
267
|
+
- 判定はmodule全体で行います。module内のどこか1か所でも通常の呼び出しが
|
|
268
|
+
あれば、その関数は独自のboundaryを失います。*さらに* `<Title />` としても
|
|
269
|
+
renderしている場合、そのinstanceは自分のsubscriptionを持たず、親が再render
|
|
270
|
+
したときにしか更新されません。この混在に対してpluginは警告を出します。
|
|
271
|
+
どちらか一方の形に統一してください。
|
|
272
|
+
- 認識するのは、その関数自身のbinding、または定義されている正確なobjectの
|
|
273
|
+
key pathを通した呼び出しだけで、同じmodule内に限ります。そのpathのどの
|
|
274
|
+
linkがoptionalでもかまいません(`ns?.group.Home()`)。同じobject literalの
|
|
275
|
+
memberの中では、`this` がそのpathとして扱われます。method、accessor、
|
|
276
|
+
`function` のproperty値、およびそれらの中に入れ子になったarrowでの
|
|
277
|
+
`this.Home()` が該当します。入れ子の `function` やclassは独自の `this` を
|
|
278
|
+
持つため、たどりません。alias(`const T = Title; T()`)、`Title.call(…)`、
|
|
279
|
+
別moduleからの呼び出しは認識しません。
|
|
280
|
+
- objectに保持された関数には、computed keyを通しても到達できます
|
|
281
|
+
(`renderers[node.type](node)`、`renderers[key]?.(arg)`、
|
|
282
|
+
`items.map(renderers[key])`、兄弟memberからの `this[key](arg)`)。そうしたaccessが到達しうるslot(同じ深さで、
|
|
283
|
+
同じ静的なprefixの下にあるもの)には、独自のboundaryを付けません。そこに
|
|
284
|
+
boundaryがあると、呼び出し元の中で可変回数実行されるためです。keyは実行時に
|
|
285
|
+
しか決まらないので、そのslotの読み取りは呼び出し元に収集 *されません*。
|
|
286
|
+
そのため、自分ではsignalを読まない呼び出し元は変換されず、slotが読むsignalの
|
|
287
|
+
変更では更新されません。呼び出し元でsignalを読むか、呼び出し元に
|
|
288
|
+
`@signalTracking` を付けてください。同じslotをmodule内で
|
|
289
|
+
`<renderers.User />` としてrenderしている場合も同様です。
|
|
290
|
+
- 読み取りを収集するのは、呼び出し元がcomponentまたは `useX` hookである場合
|
|
291
|
+
だけです。小文字のhelperを経由した呼び出し
|
|
292
|
+
(`function renderTitle() { return Title(); }`)はたどらないため、その
|
|
293
|
+
読み取りは誰もsubscribeしません。componentから直接呼び出すか、
|
|
294
|
+
`<Title />` としてrenderしてください。
|
|
295
|
+
- keyで保持された関数(`ns.Home()`)を収集するのは、このmodule内で、root
|
|
296
|
+
に束縛されたobject literalの中に定義されている場合か、ちょうど1つのmember
|
|
297
|
+
代入(`Card.Header = …`)で定義されている場合だけです。兄弟memberからの
|
|
298
|
+
`this.Home()` は、そのmemberが属するobject literalから収集します。
|
|
299
|
+
- 呼び出されていることを変換が把握できないobjectのmemberは、独自のboundaryを
|
|
300
|
+
持ったままになります。parameter経由
|
|
301
|
+
(`function render(set, key) { return set[key](); }`)、alias経由
|
|
302
|
+
(`const R = renderers[key]; R(arg)`)、computedな分割代入経由
|
|
303
|
+
(`const { [key]: R } = renderers`)、配列内のobjectから
|
|
304
|
+
(`columns.map((c) => c.Cell(row))`)、別moduleから、といった到達経路が
|
|
305
|
+
該当します。そうしたmemberをinlineで呼ぶと呼び出し元の中のhook呼び出しになり、
|
|
306
|
+
呼び出し回数が変わった時点でクラッシュします。memberに `@noSignalTracking` を
|
|
307
|
+
付けてください。memberは独自のboundaryを持たなくなり、読み取りは呼び出し元の
|
|
308
|
+
render中に実行されるため、変換された呼び出し元がそれを追跡します。その場合、
|
|
309
|
+
そのmemberを要素としてrenderすると(`<icons.Check />`)、その要素は独自の
|
|
310
|
+
subscriptionを持ちません。
|
|
311
|
+
|
|
144
312
|
## Higher-order component (HOC)
|
|
145
313
|
|
|
146
314
|
higher-order component(HOC)が返すcomponentは、自分自身の名前を持たなくても
|
|
@@ -152,7 +320,7 @@ export const withCount = (Base) => (props) => <Base {...props} count={count.valu
|
|
|
152
320
|
|
|
153
321
|
ここでcomponentは内側の関数であり、その識別子はfactory自身のbinding
|
|
154
322
|
(`withCount`)から継承します。したがって `auto` modeはこれをsubscribeし、
|
|
155
|
-
`@
|
|
323
|
+
`@signalTracking`(`@noSignalTracking` も同様)コメントは、返される関数側とfactoryの
|
|
156
324
|
宣言側のどちらに書いても適用されます。通常のclosureを巻き込まないための条件は
|
|
157
325
|
3つです。囲む関数から直接returnされていること(明示的な `return`、または
|
|
158
326
|
arrowの簡潔なbody)、返される関数自身がJSXをrenderしていること、そして囲む
|
|
@@ -180,7 +348,7 @@ renderするといった条件を満たす限り、他のhookとまったく同
|
|
|
180
348
|
です。名前を継承しないのは、返されるclosureだけです。
|
|
181
349
|
|
|
182
350
|
名前の継承はちょうど1段だけたどるため、factoryを返すfactory
|
|
183
|
-
(`(a) => (b) => (props) => …`)は解決せず、そこに書いた `@
|
|
351
|
+
(`(a) => (b) => (props) => …`)は解決せず、そこに書いた `@signalTracking` コメントは
|
|
184
352
|
無言で消えるのではなく警告として報告されます。唯一区別できない形は、factoryの
|
|
185
353
|
戻り値をそのままiteration methodへ渡す場合(`items.map(makeRow(prefix))`)です。
|
|
186
354
|
これは構文上componentを返すHOCそのものなので、独自のboundaryを持ちます。その
|
|
@@ -188,17 +356,25 @@ renderするといった条件を満たす限り、他のhookとまったく同
|
|
|
188
356
|
componentに収集させてください。
|
|
189
357
|
|
|
190
358
|
componentは、自分自身のbindingを持たなくても名前を得られる場合があります。
|
|
191
|
-
object
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
359
|
+
objectのproperty — `Card.Header = () => <p>{count.value}</p>` など — として
|
|
360
|
+
保持されているcomponent、またはobject literalのmethodとして書かれた
|
|
361
|
+
component(`export const ns = { Home() { return <p>{count.value}</p>; } }`)は、
|
|
362
|
+
`<Card.Header />` や `<ns.Row />` がそこへ到達するのと同じ、そのkeyから名前を
|
|
363
|
+
得ます(小文字のkeyは、小文字のbindingと同様に除外されたままです。getter、
|
|
364
|
+
setter、async method、generator methodも除外され、instance経由でしか到達
|
|
365
|
+
できないclassのmethodも対象外です)。また、名前を持たないdefault export
|
|
196
366
|
(`export default (props) => <p>{count.value}</p>`)は、moduleのファイル名から
|
|
197
367
|
名前を得ます。これは `import App from "./App"` が実際にそのcomponentへ与えて
|
|
198
|
-
いる識別子そのものです。ただし、class
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
368
|
+
いる識別子そのものです。ただし、classが保持する関数は意図的に対象外です。class field
|
|
369
|
+
(`class Holder { Row = () => … }`。instanceでも `static` でも)と、class内での
|
|
370
|
+
`this.Row = …` という代入(たとえばconstructor内)は変換されず、そこに付けた
|
|
371
|
+
`@signalTracking` コメントは警告を出したうえで無視されます。これらは
|
|
372
|
+
`this.Row` や `Holder.Row` 経由で到達するため、変換はそれがcomponentなのか、
|
|
373
|
+
`render()` が関数として直接呼ぶもの(`this.Header()`)やitemごとに渡すもの
|
|
374
|
+
(`items.map(this.Row)`)なのかを追跡できません。後者にhookのboundaryを入れると
|
|
375
|
+
class componentのrender中でhookが動き、Reactに拒否されます。要素として
|
|
376
|
+
(`<this.Row />`)だけrenderされる関数なら、その中で `useSignalTracking()` を
|
|
377
|
+
自分で呼べます。そうでなければ、signalはfunction componentで読んでください。
|
|
202
378
|
|
|
203
379
|
## `memo` / `forwardRef` の認識
|
|
204
380
|
|
|
@@ -214,12 +390,12 @@ re-exportの連鎖をたどらないためです。認識できなかった場
|
|
|
214
390
|
specifierを設定する(`"@app/react"` のようなbare specifierはどのファイルでも
|
|
215
391
|
一致します。相対pathは同じ綴りのファイルにしか一致しません)。
|
|
216
392
|
- 該当箇所で `memo` / `forwardRef` を `"react"` から直接importする。
|
|
217
|
-
- `@
|
|
393
|
+
- `@signalTracking` コメントか手書きの `useSignalTracking()` 呼び出しで明示的にopt-inする。
|
|
218
394
|
|
|
219
|
-
## `
|
|
395
|
+
## `useSignalTracking()` のopt-inと書き換え
|
|
220
396
|
|
|
221
|
-
`@
|
|
222
|
-
二重の `
|
|
397
|
+
`@signalTracking` と `@noSignalTracking` は、その関数だけに適用されます。pluginが
|
|
398
|
+
二重の `useSignalTracking()` 呼び出しを挿入することはありません。既存のdirect
|
|
223
399
|
import、namespace import、barrel import経由の呼び出しは、その関数のopt-inと
|
|
224
400
|
みなします。先頭文でない呼び出しと、barrel import経由の呼び出しは、いずれの
|
|
225
401
|
変換モードでもそのまま残ります。`importSource` からdirect importまたは
|
|
@@ -228,22 +404,70 @@ namespace importした先頭文の呼び出しは、`"inject"` ではそのま
|
|
|
228
404
|
managed storeの宣言と `try` / `finally` scopeに置き換わるため、関数本体は
|
|
229
405
|
そのまま残るのではなく書き換えられます。
|
|
230
406
|
|
|
231
|
-
先頭文での `
|
|
407
|
+
先頭文での `useSignalTracking()` 呼び出しが `async` 関数やgenerator関数の中にあると、
|
|
232
408
|
既定の `"managed"` 変換ではbuildが失敗し、
|
|
233
|
-
`
|
|
409
|
+
`useSignalTracking transform only supports synchronous, non-generator functions`
|
|
234
410
|
というエラーになります。この組み合わせはそもそもReactとして不正です
|
|
235
411
|
(hookは同期のfunction componentを必要とします)。関数側を直すのが望ましく、
|
|
236
412
|
どうしても現状のままbuildを通す必要がある場合は `transform: "inject"` を
|
|
237
413
|
使うと、関数を書き換えずにこの呼び出しを受け入れます。
|
|
238
414
|
|
|
415
|
+
引数のdefault値は関数本体より前に評価されるため、そこでのsignal読み取り
|
|
416
|
+
(`function Label({ v = count.value })`)はboundaryが開く前に行われ、追跡
|
|
417
|
+
されません。変換対象の関数にそうした引数がある場合、その引数と、評価順を
|
|
418
|
+
保つためにそれ以降のすべての引数を生成した引数に置き換え、boundaryの中の
|
|
419
|
+
最初の文として同じ `undefined` のときだけのdefault規則で束縛し直します。
|
|
420
|
+
引数の個数は変わらないため、`forwardRef` の `(props, ref)` という形は保たれ
|
|
421
|
+
ます。移動するdefault値が参照する名前を本体が宣言している場合や、移動する
|
|
422
|
+
引数を本体が再宣言している場合は移動を行わず、その読み取りは追跡されない
|
|
423
|
+
ままです。その場合はsignalを本体で読んでください。
|
|
424
|
+
|
|
425
|
+
この移動では、default値の規則と評価順は変わりません。TypeScriptでは、生成
|
|
426
|
+
した引数が元の型とoptionalかどうかを引き継ぐため、出力は有効なTypeScriptの
|
|
427
|
+
ままです。ただし、関数の `length` は変わることがあります。default値を持つ
|
|
428
|
+
引数は `length` に数えられませんが、それを置き換えた生成引数は数えられる
|
|
429
|
+
ためです。たとえば `function useLabel(prefix?: string, value = count.value)`
|
|
430
|
+
の `length` は、sourceでは1、変換後は2です。変換された関数の `length` には
|
|
431
|
+
依存しないでください。
|
|
432
|
+
|
|
239
433
|
## Buildへの組み込み
|
|
240
434
|
|
|
241
|
-
いずれの変換モードも再適用するとno-op
|
|
242
|
-
(Vite、webpack、Rspack)では `enforce: "pre"`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
435
|
+
いずれの変換モードも再適用するとno-opです。pluginは、対応するbundler
|
|
436
|
+
(Vite、webpack、Rspack)では `enforce: "pre"` で登録され、Viteではさらに
|
|
437
|
+
transform hookが `order: "pre"` を宣言します
|
|
438
|
+
([React Compilerと併用する場合](#react-compilerと併用する場合)を参照)。
|
|
439
|
+
Rollupにはどちらの概念もないため、このpluginを最初にlistしてください。
|
|
440
|
+
pluginやloaderの順序を自分で決められる場合は、React Compilerを実行するものや
|
|
441
|
+
JSXを変換して消すものより前にこのpluginを置いてください。依存関係と
|
|
442
|
+
JavaScript/TypeScript以外のmoduleはskipし、`.cjs` / `.cts` のmoduleには
|
|
443
|
+
一切手を加えません。pluginは拡張子を大文字・小文字を区別せずに判定するため、
|
|
444
|
+
`App.TSX` を `App.tsx` とまったく同様に変換します。ただし、その後bundlerが
|
|
445
|
+
そのファイルを同じようにcompileするかはbundler次第で、保証はありません。
|
|
446
|
+
Vite 8は `.JSX` / `.TSX` をJSX / TSXとしてcompileせず、このpluginの有無に
|
|
447
|
+
かかわらず解析に失敗します。`.ts` はJSXなしの
|
|
448
|
+
TypeScriptとして、`.tsx`、`.jsx`、JavaScriptはJSXを含めて解析します。
|
|
449
|
+
|
|
450
|
+
## Fast Refresh
|
|
451
|
+
|
|
452
|
+
この制約はv0.1.1から存在します。既定のmanaged変換とViteのOxcベースのFast
|
|
453
|
+
Refreshの組み合わせでは、変換されたcomponentのhookを変更する(hookを追加、
|
|
454
|
+
削除、並べ替える)と、Fast Refreshのstateは保持されません。refreshは
|
|
455
|
+
"Rendered more hooks than during the previous render"(または "fewer")で
|
|
456
|
+
失敗し、pageの完全なreloadが必要になります。managed変換のboundaryは
|
|
457
|
+
componentのhookを `try` blockの中に置くため、Fast Refreshはそれらのhookを
|
|
458
|
+
componentのhook signatureに含めません。検証したfixtureでは、
|
|
459
|
+
`transform: "inject"` にこの制約はありませんでした。
|
|
460
|
+
|
|
461
|
+
## React Server Components
|
|
462
|
+
|
|
463
|
+
この変換はserver moduleとclient moduleを区別しません。`"use client"` /
|
|
464
|
+
`"use server"` directiveも、bundlerのenvironmentやlayerも参照しないためです。
|
|
465
|
+
したがって `.value` を読む同期のserver componentには、client専用のrender
|
|
466
|
+
tracking hookが挿入され、serverでrenderした時点で失敗します(async関数は
|
|
467
|
+
変換しないため、asyncのserver componentは変換されません)。RSC構成では、
|
|
468
|
+
`exclude`(たとえばdirectory単位)でserver componentを変換対象から外すか、
|
|
469
|
+
個別に `@noSignalTracking` コメントでopt-outするか、`mode: "manual"` を
|
|
470
|
+
使ってください。
|
|
247
471
|
|
|
248
472
|
## License
|
|
249
473
|
|