reatom-review について
reatom-review は、Reatom v1001 の慣行に照らしてエージェントの成果物を検証するスキルです。公式リファレンスを基準に動作、API、テスト、状態管理を確認し、具体的な問題を優先して報告します。
主な機能
- 01Reatom v1001 の慣行に基づくレビュー
- 02非同期処理とコンテキスト伝播の確認
- 03状態設計と命名の点検
- 04エフェクトと購読期間の評価
- 05リファレンスに基づく修正案
活用シーン
- 01エージェントが書いた Reatom コードのレビュー
- 02古い API や誤解を招く文書の発見
- 03重要な動作をテストで確認できているか評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ reatom/ reatom --skill reatom-reviewインストール後、Claude Code で/reatom-reviewを入力して読み込まれたか確認します。
詳細
Reatom レビュー
このスキルは、エージェントの作業が Reatom v1001 のプラクティスに沿っているか検証するために使用します。懐疑的な姿勢を保ってください。目的は、不正な動作、誤解を招くドキュメント、古くなった API の使用、弱いテスト、もっともらしく見えるだけのパターンを見つけることです。
参照資料
API の使用方法、拡張機能のオプション、ドキュメントの記述を検証する前に、reatom スキルも読み込み、REFERENCE.md の関連セクションを確認してください。これは v1001 API の正式な参照資料です。実装(レビューではなく)の場合は、reatom スキルを直接使用してください。
レビューの姿勢
- 指摘事項から始めてください。正確性を確認する前に称賛してはいけません。
- 作者ではなく、レビュー対象の成果物を批判してください。
- 一般的なフロントエンドの慣習と食い違う場合は、REFERENCE.md をローカルの信頼できる情報源として扱ってください。
- 具体的なファイルやシンボルを引用し、問題が起きる原因を説明してください。
- 曖昧な助言より、正確な修正方法を優先してください。
- 特別な理由がなければ許容できない変更の場合は、その理由を尋ねるか、リスクとして指摘してください。
- TypeScript がコンパイルできるというだけで Reatom のコードを承認してはいけません。コンテキストの伝播、キャンセル、遅延評価、命名、購読をレビューしてください。
必須チェック
-
非同期の読み取りとクエリ:
- 冪等な読み取り/クエリデータには、
computed(async () => ...).extend(withAsyncData(...))を使用することを想定してください。 - 明確なクエリ以外の理由がない限り、マウント時のフェッチ、
effectでのフェッチ、ref、コンポーネントローカルの非同期状態、命令型ローダーを指摘してください。 - ミューテーション/コマンドには、必要に応じて
withAbortまたはトランザクションを加えたaction(async () => ...).extend(withAsync(...))を想定してください。 .status()が使えるのは、withAsync/withAsyncDataで{ status: true }を有効にしている場合のみです。それ以外では.ready()、.pending()、.error()を優先してください。- アクションの
.retry()にはwithAsync({ cacheParams: true })が必要です。これがないと、呼び出し時にretryが例外をスローします。Computed はオプションなしで再試行できます。 - 拡張の順序が重要です。
withAsync/withAsyncDataはwithCacheより前に適用する必要があります(withCacheの後にwithAsyncを付けると例外が発生します)。存在の有無だけでなく、すべての.extend(...)チェーンの順序を確認してください。 - 非同期ヘルパーアトムは getter です。
.data()、.ready()、.error()、submit.error()を使用してください。アトムオブジェクトをそのままの値として描画したり、分割代入したりしてはいけません。 - ステータスフラグはプロパティです。たとえば
status.isPendingであり、関数ではありません。対象の.error()アトムを使用し、status.errorを作り出さないでください。
- 冪等な読み取り/クエリデータには、
-
wrapと非同期コンテキスト:- Reatom のフレームを離れる非同期境界(fetch、タイマー、DOM の Promise など)では
await wrap(promise)を使用してください。ラップした await の後はコンテキストに戻っているため、アトム/アクションを直接呼び出します。 wrap(fn)はfnが外部の呼び出し元(DOM リスナー、タイマー、サードパーティのコールバックなど)に渡される場合にのみ使用してください。これは装飾された関数を返すものであり、fnを呼び出すものではありません。外部コールバックへの代入や受け渡しを伴わない、単独のwrap(() => atom.set(...))を指摘してください。- アクション/エフェクト/非同期 Computed 内の、意味のない
wrap(() => atom.set(...))()を指摘してください。すぐに実行するラップ済み IIFE は何も加えません。Reatom のフレーム内でatom.set(...)を直接呼び出してください。 await wrap(fetch(url)).then(...)を指摘してください。Promise チェーン全体をラップするか、各 await のステップをラップしてください。- Reatom の状態を
wrapまたはonEventなしで呼び出している.then(...)コールバック、DOM コールバック、タイマー、requestAnimationFrame、外部イベントリスナーを指摘してください。 - すべての
awaitの後を確認してください。後続コードが非同期の継続処理からアトム/アクションを呼び出す場合、通常は await する Promise をラップする必要があります。 - DOM または外部イベントを待つ場合は、生のイベントリスナーより
await wrap(onEvent(...))を優先してください。 wrap()は Reatom に対応したアクション/エフェクト/Computed/コールバックで使用し、通常の再利用可能な API ヘルパー内では使用しないでください。- async/await を
.then()チェーンに変換するダウンレベル変換は、コンテキスト伝播を壊す可能性があります。厳格なコンテキストエラーが発生している場合、そのような変換を行うビルド/テストターゲットを指摘してください。 withCallHookなどの Reatom フックに渡されるコールバックをラップするよう求めてはいけません。フックはすでに Reatom のコンテキストで実行されます。
- Reatom のフレームを離れる非同期境界(fetch、タイマー、DOM の Promise など)では
-
状態のモデリング:
- 書き込みは
.set(...)経由で行います。リアクティブアトムを引数付きで呼び出すと(counter(5))、例外が発生します。computedを引数付きで呼び出した場合も例外が発生します。位置引数による書き込みを見つけたら指摘してください。読み取りは引数なしで呼び出します。 - アクションの状態(
getCalls、action()が返すリスト)は一時的なもので、次のクリーンアップキューのティックでクリアされます。アクションの呼び出しリストを永続的な状態として保存したり、後から読み取ったりするコードを見つけたら指摘してください。ペイロードは代わりにアトムに保存します。 - コレクションプリミティブ(
reatomMap、reatomSet、reatomArray、reatomRecord、reatomLinkedList)は、アクション/イミュータブルメソッドを通じて更新します。その状態をインプレースで変更する処理(map().set(...)、array().push(...))を見つけたら指摘してください。無効化がスキップされ、等価性チェックが破損します。 .extend(...)ではアトム参照を置き換えられず、既存のキーも上書きできません。メソッド名が衝突すると実行時に例外が発生します。set、subscribe、extend、または以前の拡張メソッドをシャドーイングするキーを割り当てている拡張を見つけたら指摘してください。- 動的オブジェクト内の可変フィールドはアトム化してください。
- 項目ごとのアトムを使うとより明確になるのに、
selectedIds、checkedIds、編集マップなどを別々にした、正規化された並列 UI 状態を見つけたら指摘してください。 - アクションと純粋な変換の違い:
- あるデータ形状を別の形状にマッピングするだけの関数は、IO、
atom.set、他のアクション呼び出しを伴わないため、アクションではありません。通常の関数またはcomputedを使用してください。 - 副作用(ネットワーク、ストレージ、タイマー、DOM、ロギング)を実行する関数、または Reatom の状態を変更する関数(
atom.set、アクションの呼び出し)はアクションであり、名前を付ける必要があります。
- あるデータ形状を別の形状にマッピングするだけの関数は、IO、
- 薄い computed ラッパーを指摘してください。
computed(() => helper(model))のように、helperがモデルのアトムゲッターを読み取って派生値を返すだけの場合です。このヘルパーは、Reatom の外で再利用できるようにすることなくcomputedを重複させています。派生処理はcomputedの本体に記述するか、親にwithComputed/.extendで追加してください。通常のヘルパーと、それをそのまま通すだけのcomputedに分割しないでください。 - アトムを含むモデルを受け取り、リアクティブな派生処理のために
.data()/アトムゲッターを呼び出す通常のヘルパーを指摘してください。こうしたヘルパーはリアクティブなグラフを隠し、薄い computed ラッパーを招き、computed/アクションのフレーム外で呼び出された場合に安全ではありません。 - 直接の
atom.setは、ローカルな更新や単純な更新であれば問題ありません。値をアトムに渡すだけの「アイデンティティ」アクションは指摘してください。 - 副作用を伴う複雑な変換や複数ステップのフローは、名前付きアクションにしてください。
- 相対する状態とアクションは、兄弟としてエクスポートして散在させるのではなく、
.extend(...)またはwithActions(...)を使って親にまとめてください。 reatomBooleanやreatomRouteのようなコアのパターンに従ってください。親アトムを作成してから、関連するメソッド、子 computed、ローダー、ルートファクトリー、ヘルパーを.extendで追加します。- スコープ付き状態には、computed ファクトリーパターンを検討してください。
computedがスコープキーを読み取り、そのスコープ用のアトム/アクション/フォームを返すため、キーを変更すると内部のグラフが置き換わります。 - computed ファクトリーは汎用的なものであり、ルーター専用ではありません。選択されたエンティティ、編集セッション、モーダル、タブや、独自の状態のライフタイムを持つ名前付きの作業単位の周辺でも使用されているか確認してください。
- ファクトリーの契約は意図的に選択してください。スコープが非アクティブな状態を通常の状態として扱う場合は
nullを返し、名前付きスコープ外での読み取りがバグである場合は例外をスローしてください。
- 書き込みは
-
命名と追跡可能性:
- アトム、算出値、エフェクト、アクションには名前を付けます。
- アトム/アクション/モデルのファクトリーには、汎用的な動詞(
get*、create*など)ではなく、reatom*プレフィックス(例:reatomFolderTreeNodeUi、reatomGalleryImage、reatomUser)を使用します。名前付きのアトム、算出値、エフェクト、アクションを生成するファクトリー関数でありながら、単なるゲッターやコンストラクターのように見えるものを指摘してください。 - ネストされた名前や動的な名前では、
users.page、users#${id}.name、${target.name}.readyのように構造を維持します。 - ポインターの移動、スクロール、リサイズ、アニメーションのティックに関連付けられたアトム、算出値、エフェクト、アクションなど、ホットパス上にある状態やログが多くなりやすい状態とアクションのトレース名では、該当するセグメントの先頭に
_を付けます。例:lightbox._panMove、lightbox._controlsActivity、lightbox._hideControlsAfterInactivity、imageGrid._width。ログを読みやすく保てます。 - 共有モデルや例にある、名前のないプリミティブを指摘してください。
-
エフェクト、フック、サブスクリプション:
- 派生状態には
computedを、副作用にはeffectを使用します。 computedは遅延評価されます。読み込みが期待されるデータにサブスクライバーがあるか、明示的なルート/レンダリング経路があるか確認します。- サブスクライブされていない
computedは読み取るたびに再検証され、サブスクライブされているものはプッシュ方式でキャッシュされます。負荷の高い、サブスクライブされていないcomputedをホットループで読み取るコードや、ホットパス上にサブスクライバーのない高コストなcomputedがあれば指摘してください。withMemoまたはサブスクリプションの使用を検討します。 - サブスクライバーのコールバックとキュー内のエフェクトは非同期(マイクロタスク)でフラッシュされます。
.set(...)の直後にサブスクライバーが同期的に呼び出されたとアサートするコード(特にテスト)があれば指摘してください。マイクロタスク/sleep(0)を待つか、アトムを直接読み取ります。 - デフォルトのグローバルコンテキストを共有するテストでは、
context.reset()(またはcontext.startによるスコープ設定)で状態を分離する必要があります。ケース間でアトムの状態が漏れるテストスイートがあれば指摘してください。 effectは遅延評価ではありません。作成時に自動でサブスクライブされるため、モジュールスコープのeffect(...)はインポート時に即座に接続し、.unsubscribe()されるまで実行され続けます。- コンポーネント/機能のスコープに属するエフェクトを「状態をモデルに置く」ためにモジュールスコープへ移動している場合は指摘してください。マウント/表示状態に応じたスコープが失われ、機能を閉じた後も(タイマー上でループする場合も含めて)実行され続けます。
- 機能スコープのエフェクトは、次の順に機能の境界で開始することを推奨します。
- ルートローダー/ルートの
render初期化(最適):ローダーまたはルート所有の初期化アクションで、機能スコープが開いたときにエフェクトを作成します。ルートのアンマウント時またはスコープ変更時に中止/切断します。 - 機能モデル上の明示的な名前付き
init/startアクション:機能を開いたときに一度だけ呼び出します(ライトボックスを開く、パネルをマウントする、セッションを開始する場合など)。 - コンポーネントのマウント/
refのクリーンアップ(許容されますが、設計としては劣ります):マウントされたスコープ内でエフェクトを作成し、クリーンアップ時に.unsubscribe()を呼び出します。
- ルートローダー/ルートの
- エフェクトがフックの対象アトムを直接または間接的に読み取らない場合に限り、
withConnectHookとeffectの組み合わせは適切なパターンです。エフェクトが接続フックを所有する同じアトムに依存する場合、接続/サブスクライブの無限ループが発生する可能性があります。target.extend(withConnectHook(() => effect(() => target())))などは指摘してください。 withConnectHookが適切な場合は、エフェクトが依存してはならないスコープアンカーに付与します(たとえば、slideshowPlayingを読み取るスライドショータイマーでは、slideshowPlaying自体ではなくlightboxOpenを使用します)。- すでにモジュールスコープにある
effectに対して、コンポーネントが.subscribe()/.unsubscribe()を呼び出している場合は指摘してください。エフェクトは作成時に自動でサブスクライブされているため、コンポーネント側のサブスクリプションは冗長であり、元の自動サブスクリプションはリークします(エフェクトはアンマウント時に切断されません)。 - 外部のプッシュソース(
ResizeObserver、IntersectionObserver、matchMedia、ソケット)を接続駆動型アトムにつなぐ場合は、オブザーバーを設定して兄弟の結果アトムに書き込むeffectではなく、reatomObservableを推奨します。 - フックの対象に依存しない遅延外部サブスクリプション/ポーリングには
withConnectHookを使用し、クリーンアップと中止の動作を確認します。 - アトム同士の同期に
withChangeHookを使用しないでください。代わりにcomputedまたはwithComputedを使用します。
- 派生状態には
-
ルーティング:
- ルートデータには
reatomRouteローダーを優先してください。ローダーはwithAsyncDataを備えた非同期 computed です。 - ルートローダーは computed ファクトリーの真髄です。ルートのマッチ/パラメーターがスコープを定義し、そのスコープが変わるとローダーが作成したモデルは置き換えられます。
- ルートローダー/ルート所有の初期化は、機能スコープのエフェクト(タイマー、ポーリング、セッションの接続)を開始する場所としても推奨されます。モジュールレベルのエフェクトや、エフェクトが読み取る atom への
withConnectHookの使用は避けてください。 route.match()を手動で確認してnullを返すコンポーネントには注意してください。ルートのrenderオプションとレイアウト/アウトレットを優先してください。- 型が重要な場合はスキーマで URL パラメーター/検索値を検証し、文字列パラメーターは数値だと決めつけずに変換してください。
- SPA のインターセプトが想定される場合、ルートナビゲーションに
.go(...)を使い、リンクに.path(...)を使っていることを確認してください。 - ルートパスの先頭に
/は付けません。route.go()が受け取るのはパス文字列ではなくパラメーターです。 - ローダーが受け取るのは、パラメーターと検索値をマージしたオブジェクト 1 つです。
(params, search)は誤りです。 renderはルートのオプションです。構築後のroute.renderは computed 出力であり、代入可能なコールバックではありません。route.render(self)内で作成して UI に渡すコールバックにも、引き続きwrap(...)が必要です。- 認証、リダイレクト、機能ゲートは、null 許容のローダーペイロードではなく、
params()/親ガードに配置してください。 - ガードまたは URL フック内のリダイレクトは冪等にし、
.go(..., true)の前に URL の所有権を確認してください。 - レイアウト配下のインデックスルートでアクティブ状態を判定する場合は、
exact()を優先してください。match()は子孫ルートに対しても true のままです。
- ルートデータには
-
中断、サンプリング、並行処理:
take(...)とonEvent(...)はプロミスを返すため、非同期アクション/エフェクト内ではawait wrap(...)を使用してください。race(...)が想定するのはabortVar.createAndRunが返す制御されたプロミスであり、通常のプロミスではありません。- 中断可能な Reatom コンテキスト内のフェッチでは、
signal: abortVar.require().signalを渡してください。 - 非同期アクション/エフェクト/ポーリングを含む同期モデルを返す computed ファクトリーでは、外側のファクトリーに
withAbort()が必要です。withAsyncDataを備えた非同期 computed/ローダーには、すでに中断機能があります。 - ファクトリーの依存関係を確認してください。読み取りのたびに内部モデルが再作成される可能性があります。スコープ化されたグラフを再構築する入力を一部に限定するには、変動する入力を分離する、導出を外に移す、または
peek/memoを使用してください。 - デバウンスのような動作には、
withAbort()とawait wrap(sleep(ms))の組み合わせを優先してください。 - コンポーネント/エフェクト内でのタイマー管理(
setTimeout、setInterval、ローカルのタイマーハンドル、手動のclear*、アンマウント時のtry/catch)には注意してください。状態に応じたタイマーには中断可能なeffect(async () => { await wrap(sleep(ms)); ... })を、デバウンス/スロットルコマンドにはaction(...).extend(withAbort())を優先してください。 - フロー上で明示的に必要な場合を除き、中断による拒否をビジネスエラーとして扱わないでください。
-
フォーム:
- フォームには
reatomForm、reatomFieldSet、reatomFieldを優先してください。 - 非同期バリデーションには
wrapを使用し、依存するバリデーションでは他のフィールドをリアクティブに読み取れます。 - 送信ハンドラーでは
submit.error()用にエラーを throw し、ペイロードの型を明示してください。 - ルートに紐づくフォームは通常、共有のモジュールレベルシングルトンではなく、ルート/スコープ化されたファクトリー内で作成してください。
- ユーザー向けのフィールド値には
field.value()/field.change(value)を優先してください。field()は基底となる状態です。 - 送信時のミューテーションは
reatomForm({ onSubmit })に配置してform.submit()を呼び出してください。生の送信アクションを分けると、バリデーションを回避する可能性があります。
- フォームには
-
永続化と URL 同期:
- アドホックなエフェクトより、
withLocalStorage、withSessionStorage、withSearchParamsなどの Reatom ヘルパーや、ストレージ固有の永続化拡張を優先してください。 - URL の状態と永続化された状態の解析・シリアライズの動作を確認してください。特に、デフォルト値と無効な入力に注意してください。
- 永続化キーはアトムごとに一意である必要があります。モデル間でキーが重複している場合や、
versionの更新とマイグレーションを伴わない形状変更があれば指摘してください。 - キャッシュされたクエリについては、
withCacheのオプションが意図に沿っているか確認してください。swrのセマンティクス、staleTime/長さの上限、ignoreAbortのデフォルト(パラメーターが空の場合に限りtrue)を確認します。非冪等なアクションにキャッシュを適用している場合は指摘してください。
- アドホックなエフェクトより、
-
マイグレーションの正確性:
- v3 時代の古い API を指摘してください:
ctx.schedule、ctx.spy、ctx.get、reatomAsync、reatomResource、reaction、atom.onChange、onConnect、withConcurrency、onCtxAbort。 - 現在の同等機能を優先してください:
wrap、アトムの直接読み取り、peek、action(...).extend(withAsync())、computed(...).extend(withAsyncData())、effect、withChangeHook、withConnectHook、withAbort、abortVar.subscribe。
- ドキュメントとサンプル:
- ドキュメントでアンチパターンを推奨コードとして提示しないでください。
- 不適切なコードを示す場合は、明確にその旨を記し、直後に推奨される Reatom のコードを示してください。
- インポートがサンプルと一致していること、スニペット内で使われている識別子が存在すること、説明文での主張がコードと一致していることを確認してください。
- 見出し、説明文、コード、API の動作に不一致があれば指摘してください。
- サンプルでは、安全でない型キャスト、名前のないアトム/アクション、Reatom の重要なパターンを見えにくくする架空の API を避けてください。
- React とアダプター:
- アトムのゲッターを呼び出すコンポーネントには
reatomComponentを使用してください。useAtomの戻り値は通常の値であり、呼び出し可能なゲッターではありません。 - アトムの読み取り/書き込みやアクションの呼び出しを行う手書きの UI コールバックには、サードパーティ製コントロールのコールバックも含め、
wrap(...)が必要です。 - 通常の関数コンポーネントの JSX 内で
wrap(...)を直接呼び出さないでください。Reatom フレーム内でラップしたコールバックを作成するか、子コンポーネントに渡してください。 - アトムをプロパティとして渡すことは、Reatom の疎結合化として有効です。Redux の考え方から、これを否定しないでください。
- DOM の
refコールバック、オブザーバーの通知、その他の Reatom 外部からのアトム書き込みには、Reatom フレームが必要です:context.start(() => ...)、wrap(...)、またはonEvent(...)を使用します。refやResizeObserverのコールバックからの、フレームを介さないatom.setは指摘してください。
- ブラウザーリソースの所有権:
URL.createObjectURLの戻り値は、それを生成した非同期 computed(または接続フック)が所有するリソースとして扱ってください。その computed が再実行/中断されたとき、またはモデルが切断されたときに、URL を取り消す必要があります。取り消し手段がないままオブジェクト URL を単なる文字列として保存している場合は指摘してください。createImageBitmap/ImageBitmap.close()、OffscreenCanvas、Worker(およびワーカープール)、ResizeObserver/IntersectionObserver、matchMediaのリスナーは、Reatom のライフサイクル(非同期 computed の中断、withConnectHook/withDisconnectHookによるクリーンアップ、abortVar.subscribe、またはonEvent)に結び付けてください。withConnectHook/withDisconnectHook/非同期 computed の中断で所有権をすでに表現できるのに、呼び出し側が明示的に呼ぶ必要のある独自のdispose()/cleanup()メソッドがあれば指摘してください。- 共有ワーカー/デコーダープールは、接続/切断を明示した名前付きサービスモデルにしてください。これにより、フォルダー/セッション/ルートのリセット時に古い処理を終了し、リークを防げます。
- モジュール構造と循環参照:
- 循環インポートを解消するためだけに、アクション/エフェクト内で
await import('./peer')またはrequire('./peer')を使っている場合は指摘してください。動的なimport()はコード分割のためのものであり、循環参照の解消には使いません。また、同期フローが非同期になり、依存関係がトレーシングから見えなくなります。 - 本質的な解決策は構造を見直すことです。共有の調整処理を上位のオーケストレーションモジュールに移す、依存関係の向きを逆にする、または依存関係を引数として渡してください。複数の関連モジュールに触れる必要のあるリセット/ティアダウンは、通常、それらを静的インポートするオーケストレーションアクションに置くべきです。
ドキュメントの不整合チェック
ドキュメント、チュートリアル、README、生成された要約、サンプルを確認する際は、次のような不整合がないか積極的に探してください。
- 説明では「クエリ/リソース/データ読み込み」とされているのに、コードでは
computed(...).extend(withAsyncData())ではなく、effect、コンポーネントのライフサイクル、ref、手動の状態 atom を使用している。 - 説明では「ミューテーション/コマンド」とされているのに、コードでは
action(...).extend(withAsync())ではなく、冪等でない書き込みに asynccomputedを使用している。 - 説明では「中断に対応」または「競合状態に安全」とされているのに、コードに
withAbort、withAsyncData、ルートローダーの動作、または await する処理を囲むwrapがない。 - 説明では Reatom の同期書き込みがラップされているとされているのに、コードでは返された関数を呼び出す、または渡すことなく
wrap(() => atom.set(...))を使用している。 - 説明ではコードが
wrapでコンテキストを維持するとされているのに、すでにフレーム内で実行される action/effect/computed の中で、直接atom.set(...)すればよい箇所にwrap(() => atom.set(...))()を使用している。 - 説明では「中断可能な fetch」とされているのに、コードで
signal: abortVar.require().signalをfetchに渡していない。 - 説明では
.status()を使用しているのに、action/computed が{ status: true }で拡張されていない。 - 説明では「ルートローダー」とされているのに、データ取得が描画されたコンポーネント内に置かれている、または
route.match()でガードされている。 - 説明ではルートローダーのパラメーターを示しているのに、コードで統合された単一のオブジェクトではなく
(params, search)を使用している。 - 説明ではルートのリダイレクト/認証ゲートとされているのに、コードでは
params()/親ガードで処理をブロックせず、nullable なローダーデータを返している。 - 説明では「現行の Reatom」とされているのに、スニペットでレガシーの
ctx、reatomResource、reatomAsync、reaction、またはonConnectを使用している。 - 説明では状態やアクションを相対的に持つモデルについて記述しているのに、スニペットでは
.extendを通じてまとめず、別々の同階層の atom/action をエクスポートしている。 - 説明では atom/action ファクトリーを示しているのに、関数名が
reatom*ではなくget*/create*になっている。 - 説明では純粋なマッパー/フォーマッター/正規化関数を
action(...)でラップしているのに、その関数に IO がなく、状態も書き換えていない。 - 説明では派生状態を
computed(() => resolveX(model))として示しているのに、スニペットでは atom getter のみを読み取る通常のresolveXヘルパーを使用している。これは Reatom 外で再利用されない間接化を追加しているだけです。 - 説明ではフォーム送信時の検証とされているのに、コードでは
form.submit()を使わず、別の生の送信 action を使用している。 - 見出し/説明文ではある atom/action 名を示しているのに、スニペットでは別の名前を使用している。
- スニペットに
wrap、computed、withAsyncData、action、withAsync、またはreatomRouteなどの必須 import がない。 - サンプルでルート/検索パラメーターを、スキーマ変換/型変換なしで数値として型指定している。
- 問題のあるコードに「問題」などのラベルがなく、近くに修正版もない。
- 説明では effect が「画面/機能のスコープ内」とされているのに、モジュールレベルの
effect(...)が import 時に自動で購読され、コンポーネントから名目上再購読されるだけになっている。 - 説明では「状態をモデルに移した」とされているのに、その移動によってマウントスコープの effect が、ライフタイムの意味が異なる即時実行のモジュールレベル effect に変わっている。
- 説明では effect が「connect フックのスコープ内」とされているのに、effect がフックの対象 atom を直接または間接的に読み取り、無限の connect/subscribe ループを引き起こす可能性がある。
- 説明では「リークなし/アンマウント時にクリーンアップ」とされているのに、オブジェクト URL、ビットマップ、ワーカー、またはオブザーバーが、connect フックや abort による所有ではなく手動の
dispose()に依存している。 - 説明ではモジュールが分離されているとされているのに、action 内の
await import()/require()によって循環依存が隠されている。
よくある不整合と修正方法
命令的な Effect として実装されたクエリ
問題:
const users = atom<User[]>([], 'users')
effect(async () => {
users.set(await api.getUsers(page()))
}, 'users.fetch')
修正:
const users = computed(async () => {
return await wrap(api.getUsers(page()))
}, 'users').extend(withAsyncData({ initState: [] }))
理由:クエリデータは遅延実行され、中断に対応し、data、ready、error、status、retry、reset を提供する必要があります。
wrap の誤った連結
問題:
const response = await wrap(fetch(url)).then((res) => res.json())
data.set(response)
修正:
const response = await wrap(fetch(url))
const payload: Payload = await wrap(response.json())
data.set(payload)
理由:各非同期境界を Reatom が認識でき、トレース/キャンセルが維持されます。
文として使用された wrap(関数が呼び出されていない)
問題:
} finally {
wrap(() => activeRequests.set((count) => count - 1))
}
修正:
} finally {
activeRequests.set((count) => count - 1)
}
コールバックを外部 API に渡す場合の代替修正:
button.addEventListener(
'click',
wrap(() => counter.set((value) => value + 1)),
)
理由: wrap(fn) は外部の呼び出し元のために fn をデコレートするものであり、fn を実行するわけではありません。action/effect/async computed の内部 — await wrap(...) の後の finally を含む — では、コンテキストはすでに復元されています。atom を直接呼び出してください。
無意味な wrap IIFE
問題:
} finally {
wrap(() => activeRequests.set((count) => count - 1))()
}
修正:
} finally {
activeRequests.set((count) => count - 1)
}
理由: Reatom フレーム内での wrap(() => ...)() は単なる間接呼び出しにすぎません。wrap(fn) は DOM/タイマー/サードパーティコードに渡すコールバック用に、await wrap(promise) は非同期境界用に取っておいてください。
非同期境界の後に wrap が欠落している
問題:
const save = action(async (form: FormState) => {
const response = await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(form),
})
savedId.set(await response.text())
}, 'form.save')
修正:
const save = action(async (form: FormState) => {
const response = await wrap(
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(form),
}),
)
const savedIdText: string = await wrap(response.text())
savedId.set(savedIdText)
}, 'form.save')
理由: この状態更新は非同期処理の後に実行されるため、非同期境界で Reatom コンテキストを保持する必要があります。
コールバックがコンテキストなしで Reatom を呼び出している
問題:
addEventListener('online', () => {
online.set(true)
})
修正:
onEvent(globalThis, 'online', () => {
online.set(true)
})
生のコールバック API を使用しなければならない場合の代替修正:
addEventListener(
'online',
wrap(() => {
online.set(true)
}),
)
理由: コールバックも非同期のエントリーポイントです。コンテキストを保持するか、Reatom の abort 対応イベントヘルパーを使用してください。
await されたイベントがラップされていない
問題:
const confirm = action(async (button: HTMLButtonElement) => {
await onEvent(button, 'click')
confirmed.set(true)
}, 'confirm')
修正:
const confirm = action(async (button: HTMLButtonElement) => {
await wrap(onEvent(button, 'click'))
confirmed.set(true)
}, 'confirm')
理由: onEvent(...) は promise を返します。非同期の action/effect 内では wrap を通して await してください。
abort シグナルのない中断可能な fetch
問題:
const user = computed(async () => {
const response = await wrap(fetch(`/api/users/${userId()}`))
const payload: unknown = await wrap(response.json())
return parseUser(payload)
}, 'user').extend(withAsyncData())
修正:
const user = computed(async () => {
const response = await wrap(
fetch(`/api/users/${userId()}`, {
signal: abortVar.require().signal,
}),
)
const payload: unknown = await wrap(response.json())
return parseUser(payload)
}, 'user').extend(withAsyncData())
理由: withAsyncData、ルートローダー、withAbort、および abort 対応の effect は Reatom フレームをキャンセルできます。fetch も同じ abort シグナルを受け取るべきです。
有効化せずに使用された status
問題:
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync())
const status = submit.status()
修正:
const submit = action(async () => {
await wrap(api.save(form()))
}, 'form.submit').extend(withAsync({ status: true }))
const status = submit.status()
代替修正:
const ready = submit.ready()
const pending = submit.pending()
const error = submit.error()
理由: status は非同期拡張ではデフォルトで無効になっています。完全な status モデルが必要な場合にのみ { status: true } を使用してください。
恒等アクション
問題:
const query = atom('', 'search.query')
const setQuery = action((next: string) => query.set(next), 'search.query.set')
修正:
const query = atom('', 'search.query')
query.set('next value')
理由: 単純なローカル更新に転送アクションは必要ありません。アクションは副作用と状態変更を伴うフローに使用し、純粋なデータマッピングには使用しないでください。
プレーンなヘルパーに対する薄い computed ラッパー
問題:
export function resolveDownloadUrl(image: ReatomImage): string {
return image.fullImageUrl.data() ?? image.thumbnail.data()?.url ?? ''
}
const downloadUrl = computed(
() => resolveDownloadUrl(imageModel),
`${name}.display.downloadUrl`,
)
修正:
const downloadUrl = computed(
() =>
imageModel.fullImageUrl.data() ?? imageModel.thumbnail.data()?.url ?? '',
`${name}.display.downloadUrl`,
)
同じ派生をテストや非 Reatom コードで再利用する場合の代替修正: プレーンなデータ(URL、DTO)に対する純粋関数を保ち、atom を持つモデルには置かないでください。computed にその形状へと atom をマップさせてください。
理由: ヘルパーに委譲するだけで atom を読み取る computed は、トレーサビリティの利点なしに間接参照を追加するだけです。computed の本体(または親に対する withComputed)が派生であり、プレーンなヘルパーは atom の上に影の層としてではなく、プレーンな値に属します。
getter のような名前の atom ファクトリ
問題:
export const getFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})
修正:
export const reatomFolderTreeNodeUi = (folderPath: string) => ({
expanded: reatomBoolean(false, `folderTree.${folderPath}.expanded`),
isSelected: computed(
() => currentFolder()?.path === folderPath,
`folderTree.${folderPath}.isSelected`,
),
})
理由: Reatom のファクトリはトレースされた atom とアクションを生成します。reatom* はその契約を示し、reatomBoolean、reatomRoute、reatomForm といったコアヘルパーと一致します。単なる get* / create* という名前は、ネストされた unit のライフサイクルと命名規則を隠してしまいます。
兄弟エクスポートとして散らばった相対的な状態
問題:
export const search = atom('', 'search')
export const searchIsEmpty = computed(
() => search().trim() === '',
'search.isEmpty',
)
export const clearSearch = action(() => search.set(''), 'search.clear')
修正:
export const search = atom('', 'search').extend((target) => ({
isEmpty: computed(() => target().trim() === '', `${target.name}.isEmpty`),
clear: action(() => target.set(''), `${target.name}.clear`),
}))
理由: 相対的な状態とアクションは、reatomBoolean が boolean アクションをグループ化し、reatomRoute が go、loader、render、および子ルートヘルパーをアタッチするのと同様に、親モデルに置くべきです。
Atom 化の代わりに並列な UI 状態
問題:
const users = atom<UserDto[]>([], 'users')
const selectedUserIds = atom<Set<string>>(new Set(), 'users.selectedIds')
修正:
type UserModel = UserDto & {
selected: Atom<boolean>
}
const users = atom<UserModel[]>([], 'users').extend((target) => ({
fromDto(items: UserDto[]) {
target.set(
items.map((item) => ({
...item,
selected: atom(false, `users#${item.id}.selected`),
})),
)
},
}))
理由: アイテムごとの可変状態は、並列構造やリスト全体の更新を避けるため、そのアイテムの近くに置くべきです。
手動のルートレンダリング
問題:
export function UsersPage() {
if (!usersRoute.match()) return null
return <Users />
}
修正:
export const usersRoute = layoutRoute.reatomRoute({
path: 'users',
render() {
return <Users />
},
})
理由: ルートの render はマウント、完全一致、ローダー、レイアウト、およびアウトレットを処理します。
コンポーネントスコープの effect をモジュールスコープに持ち上げている(永遠に即時実行)
問題:
// models/slideshow.ts
export const slideshowAutoAdvance = effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
// components/Slideshow.tsx
ref={() => {
const stop = slideshowAutoAdvance.subscribe()
return stop
}}
修正(推奨 — ルート/機能の init):
// models/lightbox.ts
export const openLightbox = action((model: GalleryImageModel) => {
lightboxImage.set(() => model)
lightboxOpen.setTrue()
startSlideshowSession()
}, 'openLightbox')
export const startSlideshowSession = action(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}, 'slideshow.startSession')
代替修正(許容可能 — 機能コンポーネントでマウント):
// components/Slideshow.tsx
ref={() => {
const {unsubscribe} = effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
return unsubscribe
}}
理由: effect(...) は作成時に自己購読するため、モジュールレベルの effect は即座に接続され、決して切断されません。コンポーネントの余分な .subscribe() は冗長であり、元の自己購読はリークします。effect は機能の境界(ルートローダー、明示的な init アクション、またはコンポーネントのマウント)で開始し、永遠に接続されたモジュールのシングルトンとして開始しないでください。
withConnectHook 自体の依存関係に対する Effect(無限ループ)
問題:
export const slideshowPlaying = reatomBoolean(false, 'slideshowPlaying').extend(
withConnectHook(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)
修正(effect が読み取らないスコープアンカーにする):
export const lightboxOpen = reatomBoolean(false, 'lightboxOpen').extend(
withConnectHook(() => {
effect(async () => {
while (peek(slideshowPlaying)) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}),
)
より良い修正(フィーチャーを開くときに明示的に初期化する):
export const startSlideshowSession = action(() => {
effect(async () => {
while (slideshowPlaying()) {
await wrap(sleep(slideshowInterval()))
navigateLightbox(1)
}
}, 'slideshow.autoAdvance')
}, 'slideshow.startSession')
理由: connect フックは、そのターゲットが購読者を獲得したときに実行されます。ネストされた effect が同じターゲットを(直接または computed を通じて)読み取ると、connect/subscribe が永久にフィードバックし続ける可能性があります。フックを別のスコープアンカーにアタッチするか、ゲートチェックにのみ peek を使用するか、ルートローダー / init アクション / コンポーネントのマウントから effect を開始してください。
オブジェクト URL がプレーンな文字列としてリークする
問題:
const previewUrl = computed(() => {
const blob = imageBlob.data()
return blob ? URL.createObjectURL(blob) : ''
}, 'image.previewUrl')
修正:
const previewUrl = computed(async () => {
const blob = await wrap(imageBlob())
if (!blob) return ''
const url = URL.createObjectURL(blob)
abortVar.subscribe(() => URL.revokeObjectURL(url))
return url
}, 'image.previewUrl').extend(withAsyncData({ initState: '' }))
なぜ: URL.createObjectURL はリソースを割り当てます。revoke を所有する computed の abort/disconnect に結び付けることで、再計算ごとに 1 つの URL をリークする代わりに、モデルが再実行または切断されたときに URL が解放されます。
コンパクトな注意点の修正
- モジュールレベルの
effect(...)-> インポート時に即時実行されます。ルートローダー、明示的な init アクション、またはコンポーネントのマウントから開始してください — 永遠に接続されたシングルトンとしてではありません。 withConnectHook+ フックターゲットを読み取るeffect-> 無限 connect ループになります。別のスコープアンカー、ゲート用のpeek、またはルート/init/コンポーネントのマウントを使用してください。- 循環を断ち切るための
await import('./peer')/require('./peer')-> モジュールを再構築するか、オーケストレーションアクションを使用してください。動的インポートはコード分割のためのものです。 - オブジェクト URL/ビットマップ/オブザーバー/ワーカーに対する手書きの
dispose()-> クリーンアップをwithConnectHook/withDisconnectHookまたは非同期 computed の abort に結び付けてください。 - DOM の
ref/オブザーバーコールバックからのatom.set-> コールバック API にwrap(() => atom.set(...))を渡すか、context.start(() => atom.set(...))を使用してください。 - 単独の文としての
wrap(() => atom.set(...))-> デッドコードです。atom.set(...)を直接呼び出すか、外部でwrap(fn)を渡してください。 - アクション/effect/computed 内での
wrap(() => atom.set(...))()-> 無意味です。atom.set(...)を直接呼び出してください。 - アトムを読み取るプレーンな
resolveXを使ったcomputed(() => resolveX(model))-> computed 内にインライン化するか、親でwithComputed経由でアタッチしてください。 - 書き込みのための
counter(5)->counter.set(5)です。リアクティブアトムに対する位置引数呼び出しでの書き込みはスローされます。 cacheParamsなしのaction.retry()->withAsync({ cacheParams: true })を使用するか、代わりに computed を再試行してください。.extend(withCache(), withAsync())-> 順序を変更:withAsync/withAsyncDataを先に、withCacheを後に。- 後でデータとして
getCalls(someAction)を読み取る -> アクション呼び出しリストは次のティックでクリアされます。ペイロードをアトムに保存してください。 reatomMap()().set(k, v)/reatomArray()().push(x)-> プリミティブのアクションを使用してください。その場での変更は無効化をスキップします。- 購読者が発火したことを期待して
.set(...)の後に同期的にアサートする -> マイクロタスクを待機してください。通知はキューに入れられます。 - テスト間で状態を共有する ->
beforeEachでcontext.reset()を実行するか、context.startでスコープしてください。 status.isPending()->status.isPendingです。ステータスフラグはプロパティです。status.error->target.error()です。エラーは非同期ターゲットアトム上に残ります。async loader(params, search)->async loader({ q, userId })です。ローダーはマージされた 1 つの params/search オブジェクトを受け取ります。- Nullable な認証ローダー ->
params() { return allowed ? {} : null }です。ガードはローダーが実行される前にルートの所有権をブロックすべきです。 route.go('/users')->usersRoute.go(params)です。ルートパスは先頭の/なしで宣言されます。- モジュールレベルのルートフォームシングルトン -> ライフタイムがルートに従う場合は、ルート/スコープ付きファクトリーまたはローダーが作成するフォームを使用してください。
キャッシュの後に順序付けられた非同期拡張
問題:
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withCache(), withAsyncData())
修正:
const users = computed(async () => {
return await wrap(api.getUsers())
}, 'users').extend(withAsyncData(), withCache())
なぜ: withAsync / withAsyncData は withCache の後にアタッチすることを拒否し(実行時 ReatomError)、非同期ミドルウェアは pending、data、status を一貫させておくためにキャッシュヒットを監視する必要があります。
アクション呼び出しリストが永続的な状態として扱われる
問題:
const addToast = action((toast: Toast) => toast, 'addToast')
const toasts = computed(
() => getCalls(addToast).map((call) => call.payload),
'toasts',
)
修正:
const toasts = atom<Toast[]>([], 'toasts').extend(
withActions((target) => ({
add: (toast: Toast) => target.set((list) => [...list, toast]),
})),
)
なぜ: アクション状態は自動クリア可能な配列であり、次のクリーンアップティックで消去されます。これはトランザクション内での呼び出しに反応するためのものであり、保存用ではありません。永続的なデータはアトムに属します。
古い v3 API
問題:
const resource = reatomResource(async (ctx) => {
const response = await ctx.schedule(fetch('/api/users'))
return response.json()
}, 'users')
修正:
const users = computed(async () => {
const response = await wrap(fetch('/api/users'))
return await wrap(response.json())
}, 'users').extend(withAsyncData())
理由:現在の Reatom では、暗黙的なコンテキスト、wrap、非同期の computed リソースを使用します。
指摘の形式
各問題について、次の形式を使用してください。
- [Severity] `path-or-symbol`: Problem statement.
Why it matters: concrete Reatom rule or failure mode.
Fix: specific code-level change.
重大度の目安:
- Critical:状態の誤り、非同期コンテキストの喪失、キャンセル/競合のバグ、ルート動作の破損、または動作しない古い API。
- High:推奨される Reatom のモデルが回避され、その結果、リーク、先行フェッチ、古いデータ、誤解を招くドキュメントのリスクが生じる場合。
- Medium:保守性、追跡可能性、命名、アトム化、またはテストが大幅に弱まる場合。
- Low:将来のエージェントを混乱させる可能性はあるものの、動作を損なう可能性は低いスタイル/ドキュメントの明確さに関する問題。
指摘が残っていない場合は、その旨を明確に述べ、特に未テストの非同期キャンセル、ルートローダーの動作、実行されていないドキュメントの例など、残存リスクを列挙してください。
最終レビューのチェックリスト
- エージェントが変更したコードとドキュメントの両方を確認しましたか?
- 一般的な React/Solid/Vue の慣習ではなく、REFERENCE.md と照らして主張を比較しましたか?
- すべての await/コールバック境界の後で、非同期コンテキストと
wrapを確認しましたか? - Reatom のフレーム内にある、単独の
wrap(() => ...)文と、無意味なwrap(() => ...)()IIFE を指摘しましたか? - 命令型フェッチ、手動ルーティング、並行する可変状態に異議を唱えましたか?
- IO も状態書き込みも行わない純粋なマッパーに
actionが使われている場合、指摘しましたか? - ヘルパーがアトムのゲッターを読み取るだけの、薄い
computed(() => helper(model))ラッパーを指摘しましたか? get*/create*という名前のアトム/アクションファクトリーではなく、reatom*が使われているか確認しましたか?- エフェクトのライフタイムを確認しましたか?(モジュールレベルの
effectの即時実行、機能の初期化(ルートローダー/初期化アクション/コンポーネントのマウント)、connect フックの依存関係の落とし穴、冗長なコンポーネントの購読) - オブジェクト URL、ビットマップ、ワーカー、オブザーバーが手動の
dispose()ではなく、Reatom のライフサイクルによって管理されているか確認しましたか? .extend(...)の順序(キャッシュより前に非同期)、retry/statusのオプション要件、拡張機能でのキーの衝突を確認しましたか?- 書き込み構文(
.setと位置引数呼び出しのどちらか)、コレクションプリミティブのインプレース変更、エフェメラルなアクション呼び出しリストへの依存を確認しましたか? - 通知のキューイング、購読解除された computed の再検証、
context.reset()によるテストコンテキストの分離など、タイミングに関する前提を確認しましたか? - 例がそのままコピーして使え、重要な import が隠されていないことを確認しましたか?
- Reatom の動作を試すテストや例を少なくとも検討せずに、承認することを避けましたか?