|
|
@@ -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後方互換性排除)
|