Yuki Takei 6 месяцев назад
Родитель
Сommit
3a85535644
1 измененных файлов с 237 добавлено и 19 удалено
  1. 237 19
      .serena/memories/apps-app-jotai-migration-progress.md

+ 237 - 19
.serena/memories/apps-app-jotai-migration-progress.md

@@ -39,7 +39,8 @@ states/
 │   ├── device.ts                   # デバイス状態 ✅
 │   ├── page.ts                     # ページUI状態 ✅
 │   ├── toc.ts                      # TOC状態 ✅
-│   ├── untitled-page.ts            # 無題ページ状態 ✅ NEW!
+│   ├── untitled-page.ts            # 無題ページ状態 ✅
+│   ├── page-abilities.ts           # ページ権限判定状態 ✅ 🆕 DERIVED ATOM!
 │   └── modal/                      # 個別モーダルファイル ✅
 │       ├── page-create.ts          # ページ作成モーダル ✅
 │       ├── page-delete.ts          # ページ削除モーダル ✅
@@ -67,6 +68,75 @@ states/
 └── context.ts                      # 共通コンテキスト ✅
 ```
 
+## 🆕 **Derived Atom採用ガイドライン(2025-09-25確立)**
+
+### 🎯 Derived Atom vs Direct Hook判定基準
+
+#### **Derived Atom化すべき条件(優先度順)**
+1. **複雑な計算ロジック**: 依存関係4個以上
+2. **高頻度での使用**: レンダリング回数が多い箇所で使用
+3. **複数コンポーネントでの共有**: 計算結果を複数箇所で使用
+4. **パフォーマンス要求**: 計算コストが高い
+
+#### **Direct Hook維持すべき条件**
+1. **シンプルな計算**: 依存関係2-3個以下
+2. **低頻度での使用**: 特定条件下でのみレンダリング
+3. **単一コンポーネント使用**: 計算結果共有の必要なし
+4. **パフォーマンス要求低**: 計算コストが軽微
+
+### 🏗️ **Derived Atom実装パターン**
+
+#### **特殊名Export方式(必須パターン)**
+```typescript
+// ~/states/page/internal-atoms.ts
+export const _atomsForDerivedAbilities = {
+  pageNotFoundAtom,
+  currentPagePathAtom,
+  isIdenticalPathAtom,
+  // ... 必要な内部atom
+} as const;
+
+// ~/states/page/index.ts(公開API)
+export { _atomsForDerivedAbilities } from './internal-atoms';
+```
+
+#### **Derived Atom + Hook実装**
+```typescript
+// Import internal atoms with special naming
+import { _atomsForDerivedAbilities as pageAtoms } from '~/states/page';
+import { _atomsForDerivedAbilities as editorAtoms } from '~/states/ui/editor';
+
+// Derived atom(内部実装)
+const isAbleToShowTagLabelAtom = atom((get) => {
+  const isNotFound = get(pageAtoms.pageNotFoundAtom);
+  const currentPagePath = get(pageAtoms.currentPagePathAtom);
+  const isIdenticalPath = get(pageAtoms.isIdenticalPathAtom);
+  const shareLinkId = get(pageAtoms.shareLinkIdAtom);
+  const editorMode = get(editorAtoms.editorModeAtom);
+
+  // undefined判定(必須)
+  if ([currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined)) {
+    return false;
+  }
+
+  // ビジネスロジック
+  const isViewMode = editorMode === EditorMode.View;
+  return !isUsersTopPage(currentPagePath!) && !isTrashTopPage(currentPagePath!)
+    && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound);
+});
+
+// Public hook(外部API)
+export const useIsAbleToShowTagLabel = (): boolean => {
+  return useAtomValue(isAbleToShowTagLabelAtom);
+};
+```
+
+#### **コードパフォーマンス最適化のポイント**
+1. **自動メモ化**: 依存atomが変わらない限り再計算されない
+2. **計算結果共有**: 複数コンポーネント間で効率的に共有
+3. **最適化された更新**: Jotaiの依存関係追跡
+4. **undefined判定**: 初期化前の状態を適切にハンドリング
+
 ### 🎯 確立された実装パターン
 
 #### パフォーマンス最適化フック分離パターン
@@ -182,7 +252,7 @@ export const useTocOptions = () => {
 };
 ```
 
-#### シンプルなBoolean状態パターン(NEW! 2025-09-11追加)
+#### シンプルなBoolean状態パターン
 ```typescript
 // Atom定義
 const isUntitledPageAtom = atom<boolean>(false);
@@ -198,6 +268,28 @@ export const useSetIsUntitledPage = () => {
 };
 ```
 
+#### 🆕 **Derived Atomパターン(高パフォーマンス)**
+```typescript
+// Derived atom(計算結果の自動メモ化・共有)
+const derivedCalculationAtom = atom((get) => {
+  const dependency1 = get(atom1);
+  const dependency2 = get(atom2);
+  
+  // undefined判定(必須)
+  if ([dependency1, dependency2].some(v => v === undefined)) {
+    return defaultValue;
+  }
+  
+  // 複雑な計算ロジック
+  return computeExpensiveCalculation(dependency1, dependency2);
+});
+
+// Public hook(シンプルな値取得のみ)
+export const useDerivedCalculation = () => {
+  return useAtomValue(derivedCalculationAtom);
+};
+```
+
 #### 使用パターン
 - **ステータスのみ必要**: `use[Modal]Status()`
 - **アクションのみ必要**: `use[Modal]Actions()`
@@ -206,6 +298,7 @@ export const useSetIsUntitledPage = () => {
 - **TOC状態**: `const tocNode = useTocNode()`, `const setTocNode = useSetTocNode()`
 - **TOCオプション**: `const { data, isLoading, error } = useTocOptions()`
 - **無題ページ状態**: `const isUntitled = useIsUntitledPage()`, `const setIsUntitled = useSetIsUntitledPage()`
+- **🆕 Derived Atom**: `const result = useDerivedCalculation()` (高パフォーマンス)
 
 #### 重要事項
 - **後方互換フックは不要**: 移行完了後は即座に削除
@@ -214,6 +307,9 @@ export const useSetIsUntitledPage = () => {
 - **RefObjectパターン**: mutableなDOM要素の管理に使用
 - **Dynamic Import**: 重いライブラリの遅延ロードでパフォーマンス最適化
 - **シンプルパターン**: SWR後方互換性が不要な場合のシンプルな実装
+- **🆕 特殊名Export**: `_atomsForDerivedAbilities`によるカプセル化維持
+- **🆕 計算結果共有**: 複数コンポーネント間での効率的な状態共有
+- **🆕 自動メモ化**: 依存atomが変わらない限り再計算されない
 
 ## ✅ 移行完了済み状態
 
@@ -224,6 +320,7 @@ export const useSetIsUntitledPage = () => {
 - ✅ **ページUI状態**: `usePageControlsX`
 - ✅ **TOC状態**: `useTocNode`, `useSetTocNode`, `useTocOptions`, `useTocOptionsReady` (2025-09-11完了)
 - ✅ **無題ページ状態**: `useIsUntitledPage`, `useSetIsUntitledPage` (2025-09-11完了)
+- ✅ **🆕 ページ権限判定状態(Derived Atom)**: `useIsAbleToShowTagLabel`, `useIsAbleToShowTrashPageManagementButtons`, `useIsAbleToShowPageManagement` (2025-09-25完了)
 
 ### データ関連状態(完了)
 - ✅ **ページ状態**: `useCurrentPageId`, `useCurrentPageData`, `useCurrentPagePath`, `usePageNotFound`, `usePageNotCreatable`, `useLatestRevision`
@@ -416,6 +513,109 @@ export const useSetIsUntitledPage = () => {
 - **複雑性排除**: 不要なwrapper関数やcallback不要
 - **型安全性**: TypeScriptによる完全な型チェック
 
+### 🆕 **ページ権限判定状態移行完了(2025-09-25完了)**
+
+#### ✅ 高パフォーマンスDerived Atom移行完了
+- ✅ **`useIsAbleToShowTagLabel`** ⭐⭐⭐ (DERIVED ATOM)
+  - **使用場所**: `PageSideContents.tsx`(高頻度レンダリング)
+  - **依存関係**: 5個のatom(pageNotFound, currentPagePath, isIdenticalPath, shareLinkId, editorMode)
+  - **パフォーマンス効果**: 最大(自動メモ化、計算結果共有)
+
+- ✅ **`useIsAbleToShowTrashPageManagementButtons`** ⭐⭐⭐ (DERIVED ATOM)
+  - **使用場所**: `TrashPageAlert.tsx`
+  - **依存関係**: 5個のatom(currentUser, currentPageId, pageNotFound, isTrashPage, isReadOnlyUser)
+  - **パフォーマンス効果**: 大(効率的な依存関係追跡)
+
+- ✅ **`useIsAbleToShowPageManagement`** ⭐⭐ (DERIVED ATOM)
+  - **使用場所**: `GrowiContextualSubNavigation.tsx`
+  - **依存関係**: 4個のatom(currentPageId, pageNotFound, isTrashPage, isSharedUser)
+  - **パフォーマンス効果**: 中(計算結果共有)
+
+#### ✅ Direct Hook維持(低複雑度)
+- ✅ **`useIsAbleToChangeEditorMode`** ⭐ (DIRECT HOOK)
+  - **理由**: シンプル(2依存:isEditable, isSharedUser)、現状で十分高性能
+  
+- ✅ **`useIsAbleToShowPageAuthors`** ⭐ (DIRECT HOOK)
+  - **理由**: 比較的シンプル(3依存:pageId, pagePath, isNotFound)、使用頻度低
+
+#### 🏗️ 技術的実装の特徴
+
+**1. 特殊名Export方式の確立**
+```typescript
+// ~/states/page/internal-atoms.ts
+export const _atomsForDerivedAbilities = {
+  pageNotFoundAtom,
+  currentPagePathAtom,
+  isIdenticalPathAtom,
+  shareLinkIdAtom,
+  currentPageIdAtom,
+  isTrashPageAtom,
+} as const;
+
+// ~/states/context.ts
+export const _atomsForDerivedAbilities = {
+  isReadOnlyUserAtom,
+  isSharedUserAtom,
+} as const;
+
+// ~/states/global/global.ts  
+export const _atomsForDerivedAbilities = {
+  currentUserAtom,
+} as const;
+```
+
+**2. 公開APIを通じたImport**
+```typescript
+// Import internal atoms with special naming
+import { _atomsForDerivedAbilities as pageAtoms } from '~/states/page';
+import { _atomsForDerivedAbilities as editorAtoms } from '~/states/ui/editor';
+import { _atomsForDerivedAbilities as globalAtoms } from '~/states/global';
+import { _atomsForDerivedAbilities as contextAtoms } from '~/states/context';
+```
+
+**3. Derived Atom + シンプルHookパターン**
+```typescript
+// Derived atom(内部実装)
+const isAbleToShowTagLabelAtom = atom((get) => {
+  const isNotFound = get(pageAtoms.pageNotFoundAtom);
+  const currentPagePath = get(pageAtoms.currentPagePathAtom);
+  const isIdenticalPath = get(pageAtoms.isIdenticalPathAtom);
+  const shareLinkId = get(pageAtoms.shareLinkIdAtom);
+  const editorMode = get(editorAtoms.editorModeAtom);
+
+  // Return false if any dependency is undefined
+  if ([currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined)) {
+    return false;
+  }
+
+  const isViewMode = editorMode === EditorMode.View;
+  return !isUsersTopPage(currentPagePath!) && !isTrashTopPage(currentPagePath!)
+    && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound);
+});
+
+// Public hook(外部API)
+export const useIsAbleToShowTagLabel = (): boolean => {
+  return useAtomValue(isAbleToShowTagLabelAtom);
+};
+```
+
+#### 🚀 パフォーマンス改善効果
+1. **自動メモ化**: 依存atomが変わらない限り、再計算されない
+2. **計算結果共有**: 複数のコンポーネントで同じ計算結果を効率的に共有
+3. **依存関係追跡**: Jotaiが効率的な更新管理を実行
+4. **コード簡素化**: hookの実装が大幅に簡素化(複雑な計算ロジック → `useAtomValue`のみ)
+
+#### 📊 移行影響範囲
+- **新規実装ファイル**: `states/ui/page-abilities.ts`(Derived Atom統合)
+- **特殊名Export追加**: `page/internal-atoms.ts`, `ui/editor/atoms.ts`, `global/global.ts`, `context.ts`
+- **使用箇所更新**: `PageSideContents.tsx`, `TrashPageAlert.tsx`, `GrowiContextualSubNavigation.tsx`
+- **旧ファイル削除**: `stores/ui.tsx`(完全削除)
+
+#### 🎯 Derived Atom採用判断の指針確立
+- **高複雑度・高使用頻度**: Derived Atom化(⭐⭐⭐)
+- **中複雑度・中使用頻度**: Derived Atom化(⭐⭐)
+- **低複雑度・低使用頻度**: Direct Hook維持(⭐)
+
 ## ✅ プロジェクト完了ステータス
 
 ### 🎯 モーダル移行プロジェクト: **100% 完了** ✅
@@ -450,44 +650,62 @@ export const useSetIsUntitledPage = () => {
 - 🏆 **完全移行**: 5個のファイル + テストファイル修正完了
 - 🏆 **旧コード削除**: `stores/ui.tsx` からの完全削除
 
+### 🆕 **🎯 ページ権限判定状態移行: 完全完了** ✅
+
+**高パフォーマンスDerived Atom移行3フック + Direct Hook維持2フック**:
+- 🏆 **Derived Atom技術確立**: 特殊名Export方式、計算結果共有、自動メモ化
+- 🏆 **移行判断指針確立**: 複雑度・使用頻度に基づく最適な実装選択
+- 🏆 **パフォーマンス向上**: 高頻度レンダリング箇所での大幅な改善
+- 🏆 **カプセル化維持**: 内部atomの適切な隠蔽と公開API設計
+
 ### 🚀 成果とメリット
-1. **パフォーマンス向上**: 不要なリレンダリングの削減、Bundle Splitting
-2. **開発体験向上**: 統一されたAPIパターン、型安全性
-3. **保守性向上**: 個別ファイル化による責務明確化、API整理
+1. **パフォーマンス向上**: 不要なリレンダリングの削減、Bundle Splitting、自動メモ化
+2. **開発体験向上**: 統一されたAPIパターン、型安全性、デバッグ性向上
+3. **保守性向上**: 個別ファイル化による責務明確化、API整理、計算結果共有
 4. **型安全性**: Jotaiによる強固な型システム
 5. **レスポンシブ対応**: 正確なデバイス幅・モバイル判定
 6. **DOM管理**: RefObjectパターンによる安全なDOM要素管理
 7. **シンプル性**: 不要な複雑性の排除、直接的なAPI設計
+8. **🆕 高性能計算**: Derived Atomによる効率的な状態派生と共有
+9. **🆕 最適化戦略**: 複雑度に基づく実装パターン選択の指針確立
 
 ### 📊 最終進捗サマリー
-- **完了**: 主要なUI状態 + ページ関連状態 + SSRハイドレーション + **全17個のモーダル** + **デバイス状態4個** + **TOC状態4個** + **無題ページ状態2個**
+- **完了**: 主要なUI状態 + ページ関連状態 + SSRハイドレーション + **全17個のモーダル** + **デバイス状態4個** + **TOC状態4個** + **無題ページ状態2個** + **🆕 ページ権限判定状態5個(Derived Atom 3個 + Direct Hook 2個)**
 - **モーダル移行**: **100% 完了** (17/17個)
 - **デバイス状態移行**: **Phase 1完了** (4/4個)
 - **TOC状態移行**: **完全完了** (4/4個)
 - **無題ページ状態移行**: **完全完了** (2/2個)
+- **🆕 ページ権限判定状態移行**: **完全完了** (5/5個)
 - **品質保証**: 全型チェック成功、パフォーマンス最適化済み
-- **ドキュメント**: 完全な実装パターンガイド確立
+- **ドキュメント**: 完全な実装パターンガイド確立、**🆕 Derived Atom採用ガイドライン確立**
 
 ## 🔮 今後の発展可能性
 
-### 次のフェーズ候補(Phase 2)
-1. **残存SWRフック**: `stores/ui.tsx` 内の残り5個のフック
-   - `usePageTreeDescCountMap` - ページツリーの子孫数管理(4ファイル使用、Map操作)
-   - `useCommentEditorDirtyMap` - コメントエディタのダーティ状態管理(1ファイル使用、Map操作)
-   - `useIsAbleToShowTrashPageManagementButtons` - ゴミ箱管理ボタン表示判定(1ファイル使用、boolean計算)
-   - `useIsAbleToShowTagLabel` - タグラベル表示判定(1ファイル使用、boolean計算)
-   - その他のable系フック - 各種UI表示判定
-2. **追加SWRフック検討**: その他のSWR使用箇所の調査
-3. **AI機能のモーダル**: OpenAI関連のモーダル状態の統合検討
-4. **エディタパッケージ統合**: `@growi/editor`内のモーダル状態の統合
+### 次のフェーズ候補(Phase 3)
+1. **追加SWRフック検討**: その他のSWR使用箇所の調査とDerived Atom化判定
+2. **AI機能のモーダル**: OpenAI関連のモーダル状態の統合検討
+3. **エディタパッケージ統合**: `@growi/editor`内のモーダル状態の統合
+4. **🆕 他モジュールへのDerived Atom適用**: 複雑な計算ロジックを持つ他のフックの調査・移行
 
 ### クリーンアップ候補
-- `stores/modal.tsx` 完全削除(既に空ファイル化済み)
-- `stores/ui.tsx` の段階的縮小検討(5個のフック残存)
+- **完了済み**: `stores/modal.tsx`、`stores/ui.tsx` 完全削除済み
 - 未使用SWRフックの調査・クリーンアップ
 
+### 🆕 **技術ベストプラクティスの確立**
+1. **特殊名Export方式**: `_atomsForDerivedAbilities` による内部atom公開
+2. **Derived Atom採用判断**: 複雑度・使用頻度による最適化戦略
+3. **計算結果共有**: 複数コンポーネント間での効率的な状態管理
+4. **カプセル化維持**: 公開APIを通じた適切なモジュール設計
+
 ## 🔄 更新履歴
 
+- **2025-09-25**: 🎉 **ページ権限判定状態移行完全完了!**
+  - useIsAbleToShowTagLabel, useIsAbleToShowTrashPageManagementButtons, useIsAbleToShowPageManagement をDerived Atom化
+  - useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors をDirect Hook維持(最適な実装選択)
+  - 特殊名Export方式(`_atomsForDerivedAbilities`)の確立
+  - Derived Atom採用ガイドライン策定
+  - パフォーマンス改善効果:自動メモ化、計算結果共有、依存関係追跡
+  - stores/ui.tsx 完全削除完了
 - **2025-09-11**: 🎉 **無題ページ状態移行完全完了!**
   - useIsUntitledPage, useSetIsUntitledPage 移行完了
   - シンプルBoolean状態パターン確立(SWR後方互換性排除)