Просмотр исходного кода

Refactor sidebar state management to use Jotai for SSR hydration and improve initialization process

Yuki Takei 8 месяцев назад
Родитель
Сommit
22e4e9b277

+ 256 - 0
apps/app/docs/plan/jotai-migration-todo.md

@@ -0,0 +1,256 @@
+# Jotai 移行 TODO リスト
+
+## 現在の進捗状況
+
+### ✅ 移行完了済み
+
+1. **サイドバー関連(完了済み)**
+   - `useDrawerOpened`: サイドバーのドロワー表示状態
+   - `usePreferCollapsedMode`: サイドバーの折りたたみモード設定(永続化含む)
+   - `useSidebarMode`: サイドバーの表示モード管理(`DRAWER`, `COLLAPSED`, `DOCK`)
+
+2. **デバイス状態(完了済み)**
+   - `useDeviceLargerThanXl`: デバイスサイズ判定
+
+3. **エディター状態(部分的)**
+   - `useEditorMode`: エディターモード管理(TODO コメント有り)
+
+## 🚧 次に移行すべき状態(優先度順)
+
+### ✅ SSR対応の改善(完了)
+
+**実施内容:**
+- ~~`usePreferCollapsedModeInitializer` を削除し、`useHydrateAtoms` ベースの `UIStateHydrator` コンポーネントに移行~~
+- ✅ `usePreferCollapsedModeInitializer` を削除し、`useHydrateAtoms` ベースの `useHydrateSidebarAtoms` フックに移行
+- HoC パターンからカスタムフックパターンに変更し、よりシンプルで自然な実装に
+- 各ページで `useHydrateSidebarAtoms` を使用してJotai atomsの初期化を統一的に実行
+- SSR/CSRハイドレーション不整合の問題を解決
+
+**変更ファイル:**
+- ~~`states/ui/UIStateHydrator.tsx`: 新規作成~~ → `states/hydrate/sidebar.ts`: 新規作成(改良)
+- `states/ui/sidebar.ts`: initializer フック削除、base atom のエクスポート追加
+- `pages/utils/commons.ts`: `useInitSidebarConfig` 関数を簡素化
+- `pages/[[...path]].page.tsx`: `useHydrateSidebarAtoms` フック使用
+- `pages/_private-legacy-pages.page.tsx`: `useHydrateSidebarAtoms` フック使用
+
+**メリット:**
+- ✅ Jotai公式のSSR対応パターンに準拠
+- ✅ 複数atomsの統一的な初期化
+- ✅ ハイドレーション不整合の解決
+- ✅ 将来の状態追加時の拡張性向上
+- ✅ HoCよりもシンプルなフックパターン
+- ✅ JSX階層がフラット
+- ✅ より自然な React Hook の使用感
+
+### 優先度 1: UI 状態(クライアントサイド完結)
+
+#### 1.1 サイドバー関連の残り
+- [ ] `useCurrentSidebarContents`: サイドバーのコンテンツタイプ
+  - **ファイル**: `/workspace/growi/apps/app/src/stores/ui.tsx:156`
+  - **使用箇所**: 
+    - `SidebarContents.tsx`
+    - `PrimaryItem.tsx`
+    - `pages/utils/commons.ts`
+  - **特徴**: 永続化必要(`scheduleToPut({ currentSidebarContents: data })`)
+
+- [ ] `useCollapsedContentsOpened`: 折りたたまれたコンテンツの開閉状態
+  - **ファイル**: `/workspace/growi/apps/app/src/stores/ui.tsx:188`
+  - **使用箇所**: 
+    - `ToggleCollapseButton.tsx`
+    - `PrimaryItem.tsx`
+    - `SidebarContents.tsx`
+    - `Sidebar.tsx`
+    - `UISettings.tsx`
+  - **特徴**: 一時的な状態(永続化不要)
+
+- [ ] `useCurrentProductNavWidth`: プロダクトナビゲーションの幅
+  - **ファイル**: `/workspace/growi/apps/app/src/stores/ui.tsx:175`
+  - **使用箇所**: 
+    - `PagePathNavSticky.tsx`
+    - `Sidebar.tsx`
+    - `pages/utils/commons.ts`
+  - **特徴**: 永続化必要(`scheduleToPut({ currentProductNavWidth: data })`)
+
+#### 1.2 ページ関連の UI 状態
+- [ ] `usePageControlsX`: ページコントロールのX座標
+  - **ファイル**: `/workspace/growi/apps/app/src/stores/ui.tsx:171`
+  - **使用箇所**: 
+    - `PagePathNavSticky.tsx`
+    - `PageControls.tsx`
+    - `PageHeader.tsx`
+  - **特徴**: 一時的な状態(永続化不要)
+
+- [ ] `useSelectedGrant`: 選択された権限設定
+  - **ファイル**: `/workspace/growi/apps/app/src/stores/ui.tsx:192`
+  - **使用箇所**: 
+    - `SavePageControls.tsx`
+    - `GrantSelector.tsx`
+    - `PageEditor.tsx`
+  - **特徴**: エディター内の一時的な状態
+
+### 優先度 2: モーダル状態
+
+#### 2.1 各種モーダルの開閉状態
+すべて `/workspace/growi/apps/app/src/stores/modal.tsx` に実装されている:
+
+- [ ] `usePageCreateModal`: ページ作成モーダル
+- [ ] `useGrantedGroupsInheritanceSelectModal`: グループ継承選択モーダル
+- [ ] `useDeleteModal`: 削除モーダル
+- [ ] `useEmptyTrashModal`: ゴミ箱空モーダル
+- [ ] `useDuplicateModal`: 複製モーダル
+- [ ] `useRenameModal`: リネームモーダル
+- [ ] `usePutBackPageModal`: ページ復元モーダル
+- [ ] `usePresentationModal`: プレゼンテーションモーダル
+- [ ] `usePrivateLegacyPagesMigrationModal`: プライベートページ移行モーダル
+
+**特徴**: すべて一時的な状態で永続化不要
+
+### 優先度 3: データ関連状態(SWR 継続使用を検討)
+
+以下は SWR での管理を継続すべきか検討が必要:
+
+- [ ] **Alert 系** (`/workspace/growi/apps/app/src/stores/alert.tsx`)
+  - `usePageStatusAlert`: ページステータスアラート
+
+- [ ] **YJS 関連** (`/workspace/growi/apps/app/src/stores/yjs.ts`)
+  - `useCurrentPageYjsData`: YJS データ
+
+- [ ] **リモートページ関連** (`/workspace/growi/apps/app/src/stores/remote-latest-page.ts`)
+  - `useRemoteRevisionId`
+  - `useRemoteRevisionBody`
+  - `useRemoteRevisionLastUpdateUser`
+  - `useRemoteRevisionLastUpdatedAt`
+
+- [ ] **編集クライアント** (`/workspace/growi/apps/app/src/stores/use-editing-clients.ts`)
+  - `useEditingClients`
+
+## 🎯 次の実装ステップ
+
+### ステップ 1: 優先度1の UI 状態を移行
+
+1. **`useCurrentSidebarContents` の移行**
+   - 新しい atom を `states/ui/sidebar.ts` に追加
+   - 永続化機能を実装(`usePreferCollapsedMode` のパターンを参考)
+   - 使用箇所を順次更新
+
+2. **`useCollapsedContentsOpened` の移行**
+   - シンプルな boolean atom として実装
+   - 使用箇所を順次更新
+
+3. **`useCurrentProductNavWidth` の移行**
+   - 永続化機能付きの atom として実装
+   - 使用箇所を順次更新
+
+4. **`usePageControlsX` の移行**
+   - シンプルな number atom として実装
+   - 使用箇所を順次更新
+
+5. **`useSelectedGrant` の移行**
+   - エディター関連の atom として `states/ui/editor.ts` に追加
+   - 使用箇所を順次更新
+
+### ステップ 2: モーダル状態の移行
+
+1. **新しいファイル作成**: `states/ui/modal.ts`
+2. **各モーダルの atom を実装**
+   - 統一的なパターンでモーダル状態を管理
+   - 開閉とデータ保持の機能を提供
+3. **既存の `stores/modal.tsx` から順次移行**
+
+### ステップ 3: 移行完了後のクリーンアップ
+
+1. **不要になったファイルの削除**
+   - `stores/ui.tsx` の移行済み部分を削除
+   - `stores/modal.tsx` の移行済み部分を削除
+
+2. **統一的なパターンの確立**
+   - 命名規則の統一
+   - ディレクトリ構造の整理
+   - ドキュメントの更新
+
+### 📋 移行時の注意点
+
+### SSR対応パターン(ハイドレーション専用モジュール方式)
+```typescript
+// states/hydrate/sidebar.ts - サイドバー状態のSSRハイドレーション
+export const useHydrateSidebarAtoms = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
+  useHydrateAtoms([
+    [preferCollapsedModeAtom, userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode],
+    // 他のサイドバー関連 atom も同様に追加
+  ]);
+};
+
+// 各ページでの使用(シンプル!)
+const MyPage = (props) => {
+  useHydrateSidebarAtoms(props.sidebarConfig, props.userUISettings);
+  // 他のロジック...
+  return <div>...</div>;
+};
+
+// 将来の拡張例
+// states/hydrate/modal.ts
+// states/hydrate/editor.ts
+```
+
+**ディレクトリ構造:**
+```
+states/
+├── ui/
+│   ├── sidebar.ts      # サイドバー状態の定義・操作
+│   ├── modal.ts        # モーダル状態の定義・操作
+│   └── editor.ts       # エディター状態の定義・操作
+└── hydrate/
+    ├── sidebar.ts      # サイドバー状態のSSRハイドレーション
+    ├── modal.ts        # モーダル状態のSSRハイドレーション (将来)
+    └── editor.ts       # エディター状態のSSRハイドレーション (将来)
+```
+
+### 永続化パターン
+```typescript
+// 永続化が必要な状態の実装パターン
+const someSettingAtom = atom(defaultValue);
+const someSettingAtomExt = atom(
+  get => get(someSettingAtom),
+  (get, set, update: ValueType) => {
+    set(someSettingAtom, update);
+    scheduleToPut({ settingKey: update });
+  },
+);
+```
+
+### 初期化パターン
+```typescript
+// サーバーサイドデータでの初期化パターン
+const initializedAtom = atom(false);
+export const useSomeSettingInitializer = (initialData: ValueType): void => {
+  const [isInitialized, setIsInitialized] = useAtom(initializedAtom);
+  const [, setSomeSetting] = useSomeSetting();
+
+  useEffect(() => {
+    if (!isInitialized) {
+      setSomeSetting(initialData);
+      setIsInitialized(true);
+    }
+  }, [isInitialized, setSomeSetting, setIsInitialized, initialData]);
+};
+```
+
+### テスト戦略
+- 各 atom の単体テスト
+- 永続化機能のテスト
+- コンポーネント結合テスト
+- E2E テストでの動作確認
+
+## 🔍 判断基準
+
+### Jotai に移行すべき状態
+- ✅ クライアントサイド完結の UI 状態
+- ✅ 同期的な状態更新
+- ✅ シンプルなデータ構造
+- ✅ コンポーネント間での状態共有が必要
+
+### SWR を継続使用すべき状態
+- ❌ サーバーからのデータフェッチが必要
+- ❌ 非同期的な状態更新
+- ❌ キャッシュ機能が重要
+- ❌ リアルタイム更新が必要

+ 42 - 21
apps/app/docs/plan/react-state-management-plan.md

@@ -97,16 +97,23 @@ PoC の結果を踏まえ、Jotai を本格的に導入し、既存の状態管
 
 
 ## 6. 今後の方針
 ## 6. 今後の方針
 
 
-### 5.1 実施した PoC の概要
+### 6.1 実施した PoC の概要
 
 
-以下の2つの状態を Jotai を使用して実装し、検証を行いました:
+以下の状態を Jotai を使用して実装し、検証を行いました:
 
 
 1.  **`useDrawerOpened`:** サイドバーのドロワー表示状態
 1.  **`useDrawerOpened`:** サイドバーのドロワー表示状態
 2.  **`usePreferCollapsedMode`:** サイドバーの折りたたみモード設定(ユーザー設定の永続化を含む)
 2.  **`usePreferCollapsedMode`:** サイドバーの折りたたみモード設定(ユーザー設定の永続化を含む)
+3.  **`useSidebarMode`:** サイドバーの表示モード管理(複数の状態を組み合わせた派生状態)
+4.  **`useDeviceLargerThanXl`:** デバイスサイズ判定
 
 
-### 5.2 実装内容
+### 6.2 実装内容
 
 
-*   新しい `states/ui.ts` モジュールを作成し、Jotai の atoms とカスタムフックを実装
+*   新しい `states/` ディレクトリ構造を作成し、責務別に分割:
+    *   `states/ui/sidebar.ts`: サイドバー関連の状態定義・操作
+    *   `states/ui/device.ts`: デバイス状態
+    *   `states/ui/editor.ts`: エディター関連の状態
+    *   `states/ui/helper.ts`: 型定義ヘルパー
+    *   `states/hydrate/sidebar.ts`: サイドバー状態のSSRハイドレーション専用
 *   既存の SWR ベースの実装を削除
 *   既存の SWR ベースの実装を削除
 *   関連するコンポーネントを新しいカスタムフックを使用するように修正:
 *   関連するコンポーネントを新しいカスタムフックを使用するように修正:
     *   `DrawerToggler`
     *   `DrawerToggler`
@@ -114,31 +121,45 @@ PoC の結果を踏まえ、Jotai を本格的に導入し、既存の状態管
     *   `EditorNavbarBottom`
     *   `EditorNavbarBottom`
     *   `Sidebar`
     *   `Sidebar`
     *   `ToggleCollapseButton`
     *   `ToggleCollapseButton`
-    *   `useSidebarMode`
+    *   `PagePathNavSticky`
+    *   `SidebarContents`
+    *   `PrimaryItems`
 *   `pages/utils/commons.ts` の初期化処理を更新
 *   `pages/utils/commons.ts` の初期化処理を更新
 
 
-### 5.3 PoC の成果
+### 6.3 PoC の成果
 
 
 *   **コードの簡潔化:** SWR の仕組みを使った複雑なカスタムフックが、シンプルな Jotai の atoms とフックに置き換わりました。
 *   **コードの簡潔化:** SWR の仕組みを使った複雑なカスタムフックが、シンプルな Jotai の atoms とフックに置き換わりました。
 *   **責務の分離:** データフェッチングとクライアント状態管理の役割が明確に分かれました。
 *   **責務の分離:** データフェッチングとクライアント状態管理の役割が明確に分かれました。
 *   **実装の直感性:** Jotai の API は React の `useState` に近く、理解しやすい実装となりました。
 *   **実装の直感性:** Jotai の API は React の `useState` に近く、理解しやすい実装となりました。
 *   **TypeScript との親和性:** Jotai は優れた型推論をサポートしており、型安全な実装が実現できました。
 *   **TypeScript との親和性:** Jotai は優れた型推論をサポートしており、型安全な実装が実現できました。
+*   **パフォーマンス改善:** 必要な箇所のみの再レンダリングが実現され、パフォーマンスが改善されました。
+*   **保守性の向上:** 状態の依存関係が明確になり、デバッグが容易になりました。
 
 
-### 5.4 今後の方針
+### 6.4 次の実装フェーズ
 
 
-PoC の結果を踏まえ、以下の方針で Jotai への移行を進めることを推奨します:
+PoC の成功を受けて、以下の段階的な移行を実施します:
 
 
-1.  **段階的な移行:**
-    *   まず `useSWRStatic` で管理されている他の UI 状態(`useCurrentSidebarContents` など)を移行
-    *   次に `useContextSWR` による状態管理を見直し、適切なものを Jotai に移行
-2.  **パターン確立:**
-    *   PoC で確立したパターン(`states/` ディレクトリへの実装、命名規則など)を踏襲
-    *   永続化が必要な状態については `usePreferCollapsedMode` の実装パターンを参考に
-3.  **テスト戦略:**
-    *   Jotai の状態に対するユニットテストの方針を確立
-    *   E2E テストでの状態管理変更の影響確認方法を整備
-4.  **ドキュメント整備:**
-    *   Jotai での状態管理パターンをチーム内で共有
-    *   新規開発時の状態管理の判断基準(SWR vs Jotai)を明確化
+#### フェーズ 1: UI 状態の移行(実施中)
+*   `useCurrentSidebarContents`: サイドバーのコンテンツタイプ(永続化必要)
+*   `useCollapsedContentsOpened`: 折りたたまれたコンテンツの開閉状態
+*   `useCurrentProductNavWidth`: プロダクトナビゲーションの幅(永続化必要)
+*   `usePageControlsX`: ページコントロールのX座標
+*   `useSelectedGrant`: 選択された権限設定
 
 
-この方針で進めることで、段階的かつ安全に状態管理の改善を実現できると考えられます。
+#### フェーズ 2: モーダル状態の移行
+*   各種モーダルの開閉状態(`stores/modal.tsx` の内容)
+*   統一的なモーダル管理パターンの確立
+
+#### フェーズ 3: その他のクライアント状態
+*   データフェッチングが不要な状態の特定と移行
+*   SWR を継続使用すべき状態の明確化
+
+### 6.5 技術的指針
+
+*   **ディレクトリ構造**: `states/ui/` 配下に機能別でファイルを分割
+*   **命名規則**: `{feature}Atom` および `use{Feature}` パターンを継続
+*   **永続化**: ユーザー設定が必要な状態は `scheduleToPut` を使用した永続化を実装
+*   **初期化**: サーバーサイドデータが必要な状態は専用の initializer フックを提供
+*   **型安全性**: TypeScript の型推論を活用し、`UseAtom` ヘルパー型を使用
+
+詳細な移行計画とTODOリストは `jotai-migration-todo.md` を参照してください。

+ 4 - 0
apps/app/src/pages/[[...path]].page.tsx

@@ -32,6 +32,7 @@ import type { CurrentPageYjsData } from '~/interfaces/yjs';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
 import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
+import { useHydrateSidebarAtoms } from '~/states/hydrate/sidebar';
 import {
 import {
   useCurrentUser,
   useCurrentUser,
   useIsForbidden, useIsSharedUser,
   useIsForbidden, useIsSharedUser,
@@ -406,6 +407,9 @@ type LayoutProps = Props & {
 }
 }
 
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // Hydrate sidebar atoms with server-side data
+  useHydrateSidebarAtoms(props.sidebarConfig, props.userUISettings);
+
   // init sidebar config with UserUISettings and sidebarConfig
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
 

+ 4 - 0
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -11,6 +11,7 @@ import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { useHydrateSidebarAtoms } from '~/states/hydrate/sidebar';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
@@ -62,6 +63,9 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsEnabledMarp(props.isEnabledMarp);
   useIsEnabledMarp(props.isEnabledMarp);
 
 
+  // Hydrate sidebar atoms with server-side data
+  useHydrateSidebarAtoms(props.sidebarConfig, props.userUISettings);
+
   // init sidebar config with UserUISettings and sidebarConfig
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
 

+ 3 - 3
apps/app/src/pages/utils/commons.ts

@@ -14,7 +14,6 @@ import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageDocument } from '~/server/models/page';
 import type { PageDocument } from '~/server/models/page';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-utils';
 import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-utils';
-import { usePreferCollapsedModeInitializer } from '~/states/ui/sidebar';
 import { useCurrentProductNavWidth, useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentProductNavWidth, useCurrentSidebarContents } from '~/stores/ui';
 import { getGrowiVersion } from '~/utils/growi-version';
 import { getGrowiVersion } from '~/utils/growi-version';
 
 
@@ -184,8 +183,9 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
 };
 };
 
 
 export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
 export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
-  // Initialize with user preference from DB if available, otherwise use system default
-  usePreferCollapsedModeInitializer(userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode);
+  // NOTE: UI state initialization is now handled by useHydrateSidebarAtoms hook
+  // This function is kept for backwards compatibility with existing SWR-based states
+  // TODO: Remove this function after migrating all UI states to Jotai
 
 
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);

+ 23 - 0
apps/app/src/states/hydrate/sidebar.ts

@@ -0,0 +1,23 @@
+import { useHydrateAtoms } from 'jotai/utils';
+
+import type { ISidebarConfig } from '../../interfaces/sidebar-config';
+import type { IUserUISettings } from '../../interfaces/user-ui-settings';
+import { preferCollapsedModeAtom } from '../ui/sidebar';
+
+/**
+ * Hook for hydrating sidebar-related UI state atoms with server-side data
+ * This should be called early in page components to ensure atoms are properly initialized before rendering
+ *
+ * @param sidebarConfig - Server-side sidebar configuration
+ * @param userUISettings - User's UI settings from database (optional)
+ */
+export const useHydrateSidebarAtoms = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
+  useHydrateAtoms([
+    // Use user preference from DB if available, otherwise use system default
+    [preferCollapsedModeAtom, userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode],
+
+    // TODO: Add other sidebar UI state atoms when migrated from SWR
+    // [currentSidebarContentsAtom, userUISettings?.currentSidebarContents ?? SidebarContentsType.TREE],
+    // [currentProductNavWidthAtom, userUISettings?.currentProductNavWidth ?? 320],
+  ]);
+};

+ 6 - 17
apps/app/src/states/ui/sidebar.ts

@@ -1,5 +1,3 @@
-import { useEffect } from 'react';
-
 import { atom, useAtom } from 'jotai';
 import { atom, useAtom } from 'jotai';
 
 
 import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { scheduleToPut } from '~/client/services/user-ui-settings';
@@ -31,23 +29,14 @@ export const usePreferCollapsedMode = (): UseAtom<typeof preferCollapsedModeAtom
   return useAtom(preferCollapsedModeAtomExt);
   return useAtom(preferCollapsedModeAtomExt);
 };
 };
 
 
-// Initialize state from server-side props only once per session
-const preferCollapsedModeInitializedAtom = atom(false);
-
-export const usePreferCollapsedModeInitializer = (initialData: boolean): void => {
-  const [isInitialized, setIsInitialized] = useAtom(preferCollapsedModeInitializedAtom);
-  const [, setPreferCollapsedMode] = usePreferCollapsedMode();
-
-  useEffect(() => {
-    if (!isInitialized) {
-      setPreferCollapsedMode(initialData);
-      setIsInitialized(true);
-    }
-  }, [isInitialized, setPreferCollapsedMode, setIsInitialized, initialData]);
-};
+// Export the base atom for SSR hydration
+export { preferCollapsedModeAtom };
 
 
+// TODO: Migrate from SWR - Current sidebar contents atom
+// const currentSidebarContentsAtom = atom<SidebarContentsType>(SidebarContentsType.TREE);
 
 
-// Sidebar mode atom
+// TODO: Migrate from SWR - Product navigation width atom
+// const currentProductNavWidthAtom = atom<number>(320);
 const sidebarModeAtom = atom(
 const sidebarModeAtom = atom(
   (get) => {
   (get) => {
     const isDeviceLargerThanXl = get(isDeviceLargerThanXlAtom);
     const isDeviceLargerThanXl = get(isDeviceLargerThanXlAtom);