|
|
@@ -37,6 +37,7 @@
|
|
|
1. **useLazyLoader**: 汎用的な動的ローディングフック (コンポーネントのアクティブ/非アクティブ状態に応じて動的ロード)
|
|
|
2. **グローバルキャッシュ**: 同じimportの重複実行防止
|
|
|
3. **責務の分離**: モーダルロジックと動的ローディングロジックの分離
|
|
|
+4. **Named Export**: コード可読性とメンテナンス性のため、named exportを標準とする
|
|
|
|
|
|
## 実装
|
|
|
|
|
|
@@ -50,9 +51,12 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
// Global cache for dynamically loaded components
|
|
|
const componentCache = new Map<string, Promise<any>>();
|
|
|
|
|
|
-const getCachedImport = <T>(
|
|
|
+/**
|
|
|
+ * Get cached import or execute new import
|
|
|
+ */
|
|
|
+const getCachedImport = <T extends Record<string, unknown>>(
|
|
|
key: string,
|
|
|
- importFn: () => Promise<{ default: React.ComponentType<T> }>
|
|
|
+ importFn: () => Promise<{ default: React.ComponentType<T> }>,
|
|
|
): Promise<{ default: React.ComponentType<T> }> => {
|
|
|
if (!componentCache.has(key)) {
|
|
|
componentCache.set(key, importFn());
|
|
|
@@ -60,6 +64,19 @@ const getCachedImport = <T>(
|
|
|
return componentCache.get(key)!;
|
|
|
};
|
|
|
|
|
|
+/**
|
|
|
+ * Clear the component cache for a specific key or all keys
|
|
|
+ * Useful for testing or force-reloading components
|
|
|
+ */
|
|
|
+export const clearComponentCache = (key?: string): void => {
|
|
|
+ if (key) {
|
|
|
+ componentCache.delete(key);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ componentCache.clear();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
/**
|
|
|
* Dynamically loads a component when it becomes active
|
|
|
*
|
|
|
@@ -80,19 +97,29 @@ const getCachedImport = <T>(
|
|
|
* // For conditional panels
|
|
|
* const AdminPanel = useLazyLoader('admin-panel', () => import('./AdminPanel'), isAdmin);
|
|
|
*/
|
|
|
-export const useLazyLoader = <T extends {}>(
|
|
|
+export const useLazyLoader = <T extends Record<string, unknown>>(
|
|
|
importKey: string,
|
|
|
importFn: () => Promise<{ default: React.ComponentType<T> }>,
|
|
|
- isActive: boolean
|
|
|
-) => {
|
|
|
+ isActive: boolean,
|
|
|
+): React.ComponentType<T> | null => {
|
|
|
const [Component, setComponent] = useState<React.ComponentType<T> | null>(null);
|
|
|
|
|
|
const memoizedImportFn = useCallback(importFn, [importKey]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (isActive && !Component) {
|
|
|
+ if (isActive && Component == null) {
|
|
|
getCachedImport(importKey, memoizedImportFn)
|
|
|
- .then(mod => setComponent(() => mod.default));
|
|
|
+ .then((mod) => {
|
|
|
+ if (mod.default) {
|
|
|
+ setComponent(() => mod.default);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ console.error(`Failed to load component with key "${importKey}": default export is missing`);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error(`Failed to load component with key "${importKey}":`, error);
|
|
|
+ });
|
|
|
}
|
|
|
}, [isActive, Component, importKey, memoizedImportFn]);
|
|
|
|
|
|
@@ -100,131 +127,470 @@ export const useLazyLoader = <T extends {}>(
|
|
|
};
|
|
|
```
|
|
|
|
|
|
+**テスト**: `apps/app/src/client/util/use-lazy-loader.spec.tsx` (12 tests passing)
|
|
|
+
|
|
|
### 2. ディレクトリ構造
|
|
|
|
|
|
```
|
|
|
apps/app/.../[ModalName]/
|
|
|
-├── index.ts # エクスポート用
|
|
|
-├── [ModalName].ts # 実際のモーダルコンポーネント
|
|
|
-└── dynamic.ts # 動的ローダー
|
|
|
+├── index.ts # エクスポート用 (named export)
|
|
|
+├── [ModalName].tsx # 実際のモーダルコンポーネント (named export)
|
|
|
+└── dynamic.tsx # 動的ローダー (named export)
|
|
|
```
|
|
|
|
|
|
-## リファクタリング手順
|
|
|
+### 3. Named Exportベストプラクティス
|
|
|
|
|
|
-### ステップ 1: ディレクトリ構造の変更
|
|
|
+**原則**: 全てのモーダルコンポーネントでnamed exportを使用する
|
|
|
|
|
|
-既存の単一ファイルを以下のように分割:
|
|
|
+**理由**:
|
|
|
+- コード可読性の向上(importで何をインポートしているか明確)
|
|
|
+- IDE/エディタのサポート向上(auto-import、リファクタリング)
|
|
|
+- 一貫性の維持(プロジェクト全体で統一されたパターン)
|
|
|
|
|
|
+**実装例**:
|
|
|
+```tsx
|
|
|
+// ❌ Default Export (非推奨)
|
|
|
+export default ShortcutsModal;
|
|
|
+
|
|
|
+// ✅ Named Export (推奨)
|
|
|
+export const ShortcutsModal = () => { /* ... */ };
|
|
|
+
|
|
|
+// dynamic.tsx
|
|
|
+export const ShortcutsModalDynamic = () => {
|
|
|
+ const Modal = useLazyLoader(
|
|
|
+ 'shortcuts-modal',
|
|
|
+ () => import('./ShortcutsModal').then(mod => ({ default: mod.ShortcutsModal })),
|
|
|
+ isOpened,
|
|
|
+ );
|
|
|
+ return Modal ? <Modal /> : <></>;
|
|
|
+};
|
|
|
+
|
|
|
+// index.ts
|
|
|
+export { ShortcutsModalDynamic as ShortcutsModal } from './dynamic';
|
|
|
+
|
|
|
+// BasicLayout.tsx
|
|
|
+import { ShortcutsModal } from '~/client/components/ShortcutsModal';
|
|
|
```
|
|
|
-Before: TemplateModal/
|
|
|
- ├── index.ts
|
|
|
- └── TemplateModal.ts
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## リファクタリング手順: 3つのケース別ガイド
|
|
|
+
|
|
|
+### 📋 事前確認: モーダルの現在の状態を判定
|
|
|
+
|
|
|
+既存のモーダルコードを確認し、以下のどのケースに該当するか判定してください:
|
|
|
+
|
|
|
+| ケース | 特徴 | 判定方法 |
|
|
|
+|--------|------|----------|
|
|
|
+| **ケースA** | Container-Presentation分離なし | 単一のコンポーネントのみ存在 |
|
|
|
+| **ケースB** | 分離済み、Container無`<Modal>` | `Substance`があるが、Containerに`<Modal>`なし |
|
|
|
+| **ケースC** | 分離済み、Container有`<Modal>` | Containerが`<Modal>`外枠を持つ |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ケースA: Container-Presentation分離されていない場合
|
|
|
+
|
|
|
+**現状**: 単一ファイルで完結しているモーダル
|
|
|
+
|
|
|
+#### 手順
|
|
|
+
|
|
|
+1. **ファイル構造変更**
|
|
|
+```
|
|
|
+Before: TemplateModal.tsx (単一ファイル)
|
|
|
After: TemplateModal/
|
|
|
├── index.ts
|
|
|
- ├── TemplateModal.ts
|
|
|
- └── dynamic.ts
|
|
|
+ ├── TemplateModal.tsx
|
|
|
+ └── dynamic.tsx
|
|
|
```
|
|
|
|
|
|
-### ステップ 2: モーダルコンポーネントの分離
|
|
|
-
|
|
|
-**Before** (TemplateModal.ts):
|
|
|
+2. **TemplateModal.tsx: Named Export化**
|
|
|
```tsx
|
|
|
-const TemplateModalSubstance = (props) => { /* heavy component */ };
|
|
|
-export const TemplateModal = () => { /* wrapper with useTemplateModal */ };
|
|
|
+// default exportの場合は変更
|
|
|
+export const TemplateModal = (): JSX.Element => {
|
|
|
+ // 既存の実装(変更なし)
|
|
|
+};
|
|
|
```
|
|
|
|
|
|
-**After** (TemplateModal/TemplateModal.ts):
|
|
|
+3. **dynamic.tsx作成**
|
|
|
```tsx
|
|
|
-// TemplateModalSubstance を TemplateModal に改名
|
|
|
-export const TemplateModal = (props: TemplateModalProps) => {
|
|
|
- // heavy component の実装
|
|
|
+import type { JSX } from 'react';
|
|
|
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
|
|
|
+import { useTemplateModalStatus } from '~/states/...';
|
|
|
+
|
|
|
+type TemplateModalProps = Record<string, unknown>;
|
|
|
+
|
|
|
+export const TemplateModalDynamic = (): JSX.Element => {
|
|
|
+ const status = useTemplateModalStatus();
|
|
|
+
|
|
|
+ const TemplateModal = useLazyLoader<TemplateModalProps>(
|
|
|
+ 'template-modal',
|
|
|
+ () => import('./TemplateModal').then(mod => ({ default: mod.TemplateModal })),
|
|
|
+ status?.isOpened ?? false,
|
|
|
+ );
|
|
|
+
|
|
|
+ // TemplateModal handles Modal wrapper and rendering
|
|
|
+ return TemplateModal ? <TemplateModal /> : <></>;
|
|
|
};
|
|
|
```
|
|
|
|
|
|
-### ステップ 3: 動的ローダーの作成
|
|
|
+4. **index.ts作成**
|
|
|
+```tsx
|
|
|
+export { TemplateModalDynamic as TemplateModal } from './dynamic';
|
|
|
+```
|
|
|
+
|
|
|
+5. **BasicLayout.tsx更新**
|
|
|
+```tsx
|
|
|
+// Before: Next.js dynamic()
|
|
|
+const TemplateModal = dynamic(() => import('~/components/TemplateModal'), { ssr: false });
|
|
|
+
|
|
|
+// After: 直接import (named)
|
|
|
+// eslint-disable-next-line no-restricted-imports
|
|
|
+import { TemplateModal } from '~/components/TemplateModal';
|
|
|
+```
|
|
|
|
|
|
-**ファイル**: `TemplateModal/dynamic.ts`
|
|
|
+---
|
|
|
|
|
|
+### ケースB: Container-Presentation分離済み、但しContainerに`<Modal>`外枠なし
|
|
|
+
|
|
|
+**現状**: `Substance`と`Container`があるが、Containerは早期returnのみで`<Modal>`を持たない
|
|
|
+
|
|
|
+**例**:
|
|
|
```tsx
|
|
|
-import React from 'react';
|
|
|
-import { Modal } from 'reactstrap';
|
|
|
-import { useLazyLoader } from '~/client/util/use-lazy-loader';
|
|
|
-import { useTemplateModal } from '~/hooks/useTemplateModal';
|
|
|
+const TemplateModalSubstance = () => { /* 全ての実装 + <Modal> */ };
|
|
|
|
|
|
-export const TemplateModalDynamic = (): JSX.Element => {
|
|
|
- const { data: templateModalStatus, close } = useTemplateModal();
|
|
|
-
|
|
|
- const TemplateModal = useLazyLoader(
|
|
|
- 'template-modal',
|
|
|
- () => import('./TemplateModal').then(mod => ({ default: mod.TemplateModal })),
|
|
|
- templateModalStatus?.isOpened ?? false
|
|
|
+export const TemplateModal = () => {
|
|
|
+ const status = useStatus();
|
|
|
+ if (!status?.isOpened) return <></>; // 早期return
|
|
|
+ return <TemplateModalSubstance />;
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+#### 手順
|
|
|
+
|
|
|
+1. **ファイル構造変更** (ケースAと同じ)
|
|
|
+
|
|
|
+2. **TemplateModal.tsxリファクタリング**: Containerに`<Modal>`を追加
|
|
|
+```tsx
|
|
|
+// Substance: <Modal>外枠を削除、<ModalHeader><ModalBody>のみに
|
|
|
+const TemplateModalSubstance = ({
|
|
|
+ someProp,
|
|
|
+ setSomeProp
|
|
|
+}: TemplateModalSubstanceProps) => {
|
|
|
+ // 重い処理・hooks
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <ModalHeader toggle={close}>...</ModalHeader>
|
|
|
+ <ModalBody>...</ModalBody>
|
|
|
+ </>
|
|
|
);
|
|
|
+};
|
|
|
|
|
|
- if (templateModalStatus == null) {
|
|
|
- return <></>;
|
|
|
- }
|
|
|
+// Container: <Modal>外枠を追加、状態管理、named export
|
|
|
+export const TemplateModal = () => {
|
|
|
+ const status = useStatus();
|
|
|
+ const { close } = useActions();
|
|
|
+ const [someProp, setSomeProp] = useState(...);
|
|
|
+
|
|
|
+ if (status == null) return <></>;
|
|
|
|
|
|
return (
|
|
|
<Modal
|
|
|
- className="template-modal"
|
|
|
- isOpen={templateModalStatus.isOpened}
|
|
|
- toggle={close}
|
|
|
- size="xl"
|
|
|
- autoFocus={false}
|
|
|
+ isOpen={status.isOpened}
|
|
|
+ toggle={close}
|
|
|
+ size="xl"
|
|
|
+ className="..."
|
|
|
>
|
|
|
- {templateModalStatus.isOpened && TemplateModal && (
|
|
|
- <TemplateModal templateModalStatus={templateModalStatus} close={close} />
|
|
|
+ {status.isOpened && (
|
|
|
+ <TemplateModalSubstance
|
|
|
+ someProp={someProp}
|
|
|
+ setSomeProp={setSomeProp}
|
|
|
+ />
|
|
|
)}
|
|
|
</Modal>
|
|
|
);
|
|
|
};
|
|
|
```
|
|
|
|
|
|
-### ステップ 4: エクスポートファイルの更新
|
|
|
+3. **dynamic.tsx, index.ts作成** (ケースAと同じ)
|
|
|
+
|
|
|
+4. **BasicLayout.tsx更新** (ケースAと同じ)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### ケースC: Container-Presentation分離済み、且つContainerに`<Modal>`外枠あり ⭐
|
|
|
|
|
|
-**ファイル**: `TemplateModal/index.ts`
|
|
|
+**現状**: 既にV2で理想的な構造になっている(最も簡単なケース)
|
|
|
|
|
|
+**例**:
|
|
|
```tsx
|
|
|
-export { TemplateModalDynamic as TemplateModal } from './dynamic';
|
|
|
+const TemplateModalSubstance = (props) => {
|
|
|
+ // 重い処理
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <ModalHeader>...</ModalHeader>
|
|
|
+ <ModalBody>...</ModalBody>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export const TemplateModal = () => {
|
|
|
+ const status = useStatus();
|
|
|
+ const { close } = useActions();
|
|
|
+
|
|
|
+ if (status == null) return <></>;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal isOpen={status.isOpened} toggle={close}>
|
|
|
+ {status.isOpened && <TemplateModalSubstance />}
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+#### 手順
|
|
|
+
|
|
|
+**最短経路**: TemplateModal.tsxの変更は**ほぼ不要**!
|
|
|
+
|
|
|
+1. **ファイル構造変更**
|
|
|
+```
|
|
|
+Before: TemplateModal.tsx (単一ファイル)
|
|
|
+After: TemplateModal/
|
|
|
+ ├── index.ts
|
|
|
+ ├── TemplateModal.tsx (移動のみ)
|
|
|
+ └── dynamic.tsx (新規)
|
|
|
+```
|
|
|
+
|
|
|
+2. **TemplateModal.tsx: Named Export確認**
|
|
|
+```tsx
|
|
|
+// default exportの場合のみ修正
|
|
|
+// Before: export default TemplateModal;
|
|
|
+// After: export const TemplateModal = ...;
|
|
|
+```
|
|
|
+
|
|
|
+3. **dynamic.tsx作成** (ケースAと同じ)
|
|
|
+
|
|
|
+4. **index.ts作成** (ケースAと同じ)
|
|
|
+
|
|
|
+5. **BasicLayout.tsx更新** (ケースAと同じ)
|
|
|
+
|
|
|
+**変更内容**: `dynamic.tsx`と`index.ts`の追加、named export化のみ
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## ケース判定フローチャート
|
|
|
+
|
|
|
+```
|
|
|
+[モーダルコード確認]
|
|
|
+ ↓
|
|
|
+[SubstanceとContainerに分離されている?]
|
|
|
+ ↓ No → ケースA: シンプル、dynamic.tsx追加 + named export化
|
|
|
+ ↓ Yes
|
|
|
+[Containerに<Modal>外枠がある?]
|
|
|
+ ↓ No → ケースB: Containerリファクタリング必要
|
|
|
+ ↓ Yes
|
|
|
+ ↓ → ケースC: ⭐最短経路、dynamic.tsx追加 + named export化のみ
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 実装例
|
|
|
+
|
|
|
+### 例1: PageAccessoriesModal (ケースB→C変換)
|
|
|
+
|
|
|
+詳細は前述のケースB手順を参照
|
|
|
+
|
|
|
+### 例2: ShortcutsModal (ケースC、最短経路) ⭐
|
|
|
+
|
|
|
+**Before**: 単一ファイル、default export
|
|
|
+```tsx
|
|
|
+// ShortcutsModal.tsx
|
|
|
+const ShortcutsModalSubstance = () => { /* ... */ };
|
|
|
+
|
|
|
+const ShortcutsModal = () => {
|
|
|
+ return (
|
|
|
+ <Modal isOpen={status?.isOpened}>
|
|
|
+ {status?.isOpened && <ShortcutsModalSubstance />}
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default ShortcutsModal; // default export
|
|
|
+```
|
|
|
+
|
|
|
+**After**: ディレクトリ構造、named export
|
|
|
+
|
|
|
+1. **ShortcutsModal/ShortcutsModal.tsx** (named export化のみ)
|
|
|
+```tsx
|
|
|
+const ShortcutsModalSubstance = () => { /* 変更なし */ };
|
|
|
+
|
|
|
+export const ShortcutsModal = () => { // named export
|
|
|
+ return (
|
|
|
+ <Modal isOpen={status?.isOpened}>
|
|
|
+ {status?.isOpened && <ShortcutsModalSubstance />}
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+2. **ShortcutsModal/dynamic.tsx** (新規)
|
|
|
+```tsx
|
|
|
+import type { JSX } from 'react';
|
|
|
+import { useLazyLoader } from '~/client/util/use-lazy-loader';
|
|
|
+import { useShortcutsModalStatus } from '~/states/ui/modal/shortcuts';
|
|
|
+
|
|
|
+type ShortcutsModalProps = Record<string, unknown>;
|
|
|
+
|
|
|
+export const ShortcutsModalDynamic = (): JSX.Element => {
|
|
|
+ const status = useShortcutsModalStatus();
|
|
|
+
|
|
|
+ const ShortcutsModal = useLazyLoader<ShortcutsModalProps>(
|
|
|
+ 'shortcuts-modal',
|
|
|
+ () => import('./ShortcutsModal').then(mod => ({ default: mod.ShortcutsModal })),
|
|
|
+ status?.isOpened ?? false,
|
|
|
+ );
|
|
|
+
|
|
|
+ return ShortcutsModal ? <ShortcutsModal /> : <></>;
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+3. **ShortcutsModal/index.ts** (新規)
|
|
|
+```tsx
|
|
|
+export { ShortcutsModalDynamic as ShortcutsModal } from './dynamic';
|
|
|
```
|
|
|
|
|
|
+4. **BasicLayout.tsx**
|
|
|
+```tsx
|
|
|
+// Before
|
|
|
+const ShortcutsModal = dynamic(() => import('~/client/components/ShortcutsModal'), { ssr: false });
|
|
|
+
|
|
|
+// After
|
|
|
+import { ShortcutsModal } from '~/client/components/ShortcutsModal';
|
|
|
+```
|
|
|
+
|
|
|
+**作業時間**: 約5分(ケースCは非常に高速)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
## チェックリスト
|
|
|
|
|
|
### 実装確認項目
|
|
|
+- [ ] **ケース判定完了**: モーダルがA/B/Cのどのケースか確認
|
|
|
- [ ] `useLazyLoader` フックが作成済み
|
|
|
-- [ ] モーダルディレクトリが作成済み(index.ts, [Modal].ts, dynamic.ts)
|
|
|
-- [ ] 実際のモーダルコンポーネントが分離済み
|
|
|
+- [ ] モーダルディレクトリが作成済み(index.ts, [Modal].tsx, dynamic.tsx)
|
|
|
+- [ ] **Named Export化**: `export const [Modal]` に変更済み
|
|
|
+- [ ] **ケースBの場合**: Containerリファクタリング完了(`<Modal>`外枠追加)
|
|
|
- [ ] 動的ローダーが `useLazyLoader` を使用
|
|
|
- [ ] エクスポートファイルが正しく設定済み
|
|
|
+- [ ] BasicLayout.tsx/ShareLinkLayout.tsxでNext.js `dynamic()`削除、直接import
|
|
|
|
|
|
### 動作確認項目
|
|
|
- [ ] ページ初回ロード時にモーダルchunkがダウンロードされない
|
|
|
- [ ] モーダルを開いた際に初めてchunkがダウンロードされる
|
|
|
- [ ] 同じモーダルを再度開いても重複ダウンロードされない
|
|
|
-- [ ] モーダルが正常に表示・動作する
|
|
|
+- [ ] **Fadeout transition正常動作**: モーダルを閉じる際にアニメーションが発生
|
|
|
+- [ ] **Container-Presentation効果**: モーダル閉じている時、Substanceがレンダリングされない
|
|
|
- [ ] TypeScriptエラーが発生しない
|
|
|
|
|
|
+---
|
|
|
+
|
|
|
## 注意点
|
|
|
|
|
|
### パフォーマンス
|
|
|
- グローバルキャッシュにより同じimportは1度だけ実行される
|
|
|
- メモ化により不要な再レンダリングを防ぐ
|
|
|
+- Container-Presentation分離により、モーダル閉じている時の無駄な処理を回避
|
|
|
|
|
|
### 型安全性
|
|
|
- ジェネリクスを使用して型安全性を保持
|
|
|
- 既存のProps型は変更不要
|
|
|
|
|
|
### 開発体験
|
|
|
+- Named exportによりコード可読性向上
|
|
|
- 既存のインポートパスは変更不要
|
|
|
- 各モーダルの状態管理ロジックは維持
|
|
|
+- ケースCの場合、既存のモーダルコードはnamed export化のみ
|
|
|
+
|
|
|
+### Fadeout Transition保証の設計原則
|
|
|
+- **Container**: 常に`<Modal>`をレンダリング(`status == null`のみ早期return)
|
|
|
+- **Substance**: `isOpened && <Substance />`で条件付きレンダリング
|
|
|
+- この設計により、`<Modal isOpen={false}>`が正しくfadeout transitionを実行できる
|
|
|
|
|
|
-## 他のモーダルへの適用
|
|
|
+---
|
|
|
|
|
|
-同じパターンを、使用頻度が高いとはいえないモーダルに関して適用する
|
|
|
+## 他のモーダルへの適用優先度
|
|
|
|
|
|
+### 高優先度(低頻度使用モーダル)
|
|
|
1. LinkEditModal
|
|
|
2. TagEditModal
|
|
|
3. ConflictDiffModal
|
|
|
-4. その他の使用頻度が高いとはいえないモーダルコンポーネント
|
|
|
+4. 管理者専用モーダル群
|
|
|
+
|
|
|
+### 中優先度(中頻度使用モーダル)
|
|
|
+- PageAccessoriesModal ✅ (完了)
|
|
|
+- ShortcutsModal ✅ (完了)
|
|
|
+- PageDuplicateModal
|
|
|
+- PageRenameModal
|
|
|
+- PageDeleteModal
|
|
|
+
|
|
|
+### 低優先度(高頻度使用モーダル)
|
|
|
+- PageCreateModal(使用頻度が非常に高いため保留)
|
|
|
+- SearchModal(使用頻度が非常に高いため保留)
|
|
|
+
|
|
|
+各モーダルで `importKey` を一意にし、適切な状態管理フックを使用することで同様の効果を得られる。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 最短経路での指示テンプレート
|
|
|
+
|
|
|
+### ケースA向け
|
|
|
+```
|
|
|
+[モーダル名]を動的ロード化してください。
|
|
|
+
|
|
|
+【現状】単一ファイル構成(Container-Presentation分離なし)
|
|
|
+
|
|
|
+【手順】
|
|
|
+1. ディレクトリ化: [Modal].tsx → [Modal]/
|
|
|
+2. Named Export化: export const [Modal] = ...
|
|
|
+3. dynamic.tsx作成: useLazyLoaderで[Modal].tsxを動的ロード
|
|
|
+4. index.ts: dynamic.tsxからexport
|
|
|
+5. BasicLayout.tsx: Next.js dynamic()削除、直接import (named)
|
|
|
|
|
|
-各モーダルで `importKey` を一意にし、適切な状態管理フックを使用することで同様の効果を得られる。
|
|
|
+【変更】[Modal].tsx本体はnamed export化のみ
|
|
|
+```
|
|
|
+
|
|
|
+### ケースB向け
|
|
|
+```
|
|
|
+[モーダル名]を動的ロード化してください。
|
|
|
+
|
|
|
+【現状】Container-Presentation分離済みだが、Containerに<Modal>外枠なし
|
|
|
+
|
|
|
+【手順】
|
|
|
+1. [Modal].tsxリファクタリング:
|
|
|
+ - Containerに<Modal>外枠を追加
|
|
|
+ - Substanceから<Modal>外枠を削除
|
|
|
+ - 必要に応じて状態をContainer→Substanceにpropsで渡す
|
|
|
+ - Container: <Modal>{isOpened && <Substance />}</Modal>
|
|
|
+ - Named Export化: export const [Modal] = ...
|
|
|
+2. dynamic.tsx作成: useLazyLoaderで[Modal]全体を動的ロード
|
|
|
+3. index.ts: dynamic.tsxからexport
|
|
|
+4. BasicLayout.tsx: Next.js dynamic()削除、直接import (named)
|
|
|
+
|
|
|
+【達成】動的ロード + Container-Presentation分離 + Fadeout transition
|
|
|
+```
|
|
|
+
|
|
|
+### ケースC向け ⭐
|
|
|
+```
|
|
|
+[モーダル名]を動的ロード化してください。
|
|
|
+
|
|
|
+【現状】理想的なContainer-Presentation分離済み(Container有<Modal>)
|
|
|
+
|
|
|
+【手順】最短経路(所要時間: 約5分)
|
|
|
+1. ディレクトリ化: [Modal].tsx → [Modal]/
|
|
|
+2. Named Export確認: export const [Modal] = ... (必要な場合のみ変更)
|
|
|
+3. dynamic.tsx作成: useLazyLoaderで[Modal]全体を動的ロード
|
|
|
+4. index.ts: dynamic.tsxからexport
|
|
|
+5. BasicLayout.tsx: Next.js dynamic()削除、直接import (named)
|
|
|
+
|
|
|
+【変更】[Modal].tsx本体はnamed export化のみ(実装は変更なし)
|
|
|
+【達成】動的ロード効果を即座に獲得
|
|
|
+```
|