[FEAT]: Refactor settings pages to use a shared layout with React Router instead of duplicating SettingsSidebar in every page #3219

Open
opened 2026-02-28 06:33:46 -05:00 by deekerman · 0 comments
Owner

Originally created by @angelplusultra on GitHub (Feb 27, 2026).

What would you like to see?

Problem

The SettingsSidebar component is individually imported and rendered inside every single settings page (27 pages total — 19 under GeneralSettings/ and 8 under Admin/). Each page duplicates the same layout wrapper pattern:

<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
  <Sidebar />
  <div
    style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
    className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
  >
    {/* Page-specific content */}
  </div>
</div>

This causes several issues:

  1. Sidebar remounts on every navigation — Because each page treats the sidebar as its own component instance, navigating between settings pages fully unmounts and remounts the sidebar. This means any transient state (scroll position, mobile menu open/closed, hover states) is lost on every page change. It also causes unnecessary re-renders and flicker.

  2. Maintenance burden — Any change to the settings layout wrapper (spacing, responsive behavior, class names) must be replicated across all 27 files. Some pages have already drifted — for example, PrivacyAndData includes light:border light:border-theme-sidebar-border in its wrapper classes while most other pages don't.

  3. Inconsistent structure — Because each page owns its own layout, there's no guarantee of structural consistency. Pages can (and do) subtly diverge in their wrapper markup.

Affected files

frontend/src/components/SettingsSidebar/index.jsx — The sidebar component itself (484 lines)

19 GeneralSettings pages:

  • GeneralSettings/LLMPreference
  • GeneralSettings/VectorDatabase
  • GeneralSettings/EmbeddingPreference
  • GeneralSettings/EmbeddingTextSplitterPreference
  • GeneralSettings/AudioPreference
  • GeneralSettings/TranscriptionPreference
  • GeneralSettings/ApiKeys
  • GeneralSettings/ChatEmbedWidgets
  • GeneralSettings/Chats
  • GeneralSettings/PrivacyAndData
  • GeneralSettings/Security
  • GeneralSettings/BrowserExtensionApiKey
  • GeneralSettings/MobileConnections
  • GeneralSettings/Settings/Interface
  • GeneralSettings/Settings/Branding
  • GeneralSettings/Settings/Chat
  • GeneralSettings/CommunityHub/Trending
  • GeneralSettings/CommunityHub/Authentication
  • GeneralSettings/CommunityHub/ImportItem/Steps

8 Admin pages:

  • Admin/Users
  • Admin/Workspaces
  • Admin/Invitations
  • Admin/Agents
  • Admin/DefaultSystemPrompt
  • Admin/SystemPromptVariables
  • Admin/Logging
  • Admin/ExperimentalFeatures

Proposed solution

Use React Router's layout route pattern to render the sidebar once at a parent level, with individual settings pages rendered as child <Outlet /> content. This is a first-class feature of React Router v6.

1. Create a SettingsLayout component:

import Sidebar from "@/components/SettingsSidebar";
import { Outlet } from "react-router-dom";
import { useMediaQuery } from "@/hooks/useMediaQuery";

export default function SettingsLayout() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  return (
    <div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
      <Sidebar />
      <div
        style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
        className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
      >
        <Outlet />
      </div>
    </div>
  );
}

2. Refactor routes in frontend/src/main.jsx:

Convert the flat settings routes into nested routes under the layout:

{
  path: "/settings",
  element: <SettingsLayout />,
  children: [
    {
      path: "llm-preference",
      lazy: async () => {
        const { default: Component } = await import("@/pages/GeneralSettings/LLMPreference");
        return { element: <AdminRoute Component={Component} /> };
      },
    },
    // ... other settings routes
  ],
}

3. Simplify each settings page — Remove the <Sidebar /> import/render and the outer layout wrapper from all 27 pages. Each page would only export its own content.

Benefits

  • Sidebar persists across navigation — No remount, no flicker, no lost state
  • Single source of truth for the settings layout wrapper
  • ~27 fewer component imports of SettingsSidebar
  • Consistent layout guaranteed by architecture rather than convention
  • Easier future changes — Modify layout in one place instead of 27
  • Better performance — Fewer DOM operations during settings navigation
Originally created by @angelplusultra on GitHub (Feb 27, 2026). ## What would you like to see? ### Problem The `SettingsSidebar` component is individually imported and rendered inside **every single settings page** (27 pages total — 19 under `GeneralSettings/` and 8 under `Admin/`). Each page duplicates the same layout wrapper pattern: ```jsx <div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex"> <Sidebar /> <div style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0" > {/* Page-specific content */} </div> </div> ``` This causes several issues: 1. **Sidebar remounts on every navigation** — Because each page treats the sidebar as its own component instance, navigating between settings pages fully unmounts and remounts the sidebar. This means any transient state (scroll position, mobile menu open/closed, hover states) is lost on every page change. It also causes unnecessary re-renders and flicker. 2. **Maintenance burden** — Any change to the settings layout wrapper (spacing, responsive behavior, class names) must be replicated across all 27 files. Some pages have already drifted — for example, `PrivacyAndData` includes `light:border light:border-theme-sidebar-border` in its wrapper classes while most other pages don't. 3. **Inconsistent structure** — Because each page owns its own layout, there's no guarantee of structural consistency. Pages can (and do) subtly diverge in their wrapper markup. ### Affected files **`frontend/src/components/SettingsSidebar/index.jsx`** — The sidebar component itself (484 lines) **19 GeneralSettings pages:** - `GeneralSettings/LLMPreference` - `GeneralSettings/VectorDatabase` - `GeneralSettings/EmbeddingPreference` - `GeneralSettings/EmbeddingTextSplitterPreference` - `GeneralSettings/AudioPreference` - `GeneralSettings/TranscriptionPreference` - `GeneralSettings/ApiKeys` - `GeneralSettings/ChatEmbedWidgets` - `GeneralSettings/Chats` - `GeneralSettings/PrivacyAndData` - `GeneralSettings/Security` - `GeneralSettings/BrowserExtensionApiKey` - `GeneralSettings/MobileConnections` - `GeneralSettings/Settings/Interface` - `GeneralSettings/Settings/Branding` - `GeneralSettings/Settings/Chat` - `GeneralSettings/CommunityHub/Trending` - `GeneralSettings/CommunityHub/Authentication` - `GeneralSettings/CommunityHub/ImportItem/Steps` **8 Admin pages:** - `Admin/Users` - `Admin/Workspaces` - `Admin/Invitations` - `Admin/Agents` - `Admin/DefaultSystemPrompt` - `Admin/SystemPromptVariables` - `Admin/Logging` - `Admin/ExperimentalFeatures` ### Proposed solution Use **React Router's layout route** pattern to render the sidebar once at a parent level, with individual settings pages rendered as child `<Outlet />` content. This is a first-class feature of React Router v6. **1. Create a `SettingsLayout` component:** ```jsx import Sidebar from "@/components/SettingsSidebar"; import { Outlet } from "react-router-dom"; import { useMediaQuery } from "@/hooks/useMediaQuery"; export default function SettingsLayout() { const isMobile = useMediaQuery("(max-width: 768px)"); return ( <div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex"> <Sidebar /> <div style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0" > <Outlet /> </div> </div> ); } ``` **2. Refactor routes in `frontend/src/main.jsx`:** Convert the flat settings routes into nested routes under the layout: ```jsx { path: "/settings", element: <SettingsLayout />, children: [ { path: "llm-preference", lazy: async () => { const { default: Component } = await import("@/pages/GeneralSettings/LLMPreference"); return { element: <AdminRoute Component={Component} /> }; }, }, // ... other settings routes ], } ``` **3. Simplify each settings page** — Remove the `<Sidebar />` import/render and the outer layout wrapper from all 27 pages. Each page would only export its own content. ### Benefits - **Sidebar persists across navigation** — No remount, no flicker, no lost state - **Single source of truth** for the settings layout wrapper - **~27 fewer component imports** of SettingsSidebar - **Consistent layout** guaranteed by architecture rather than convention - **Easier future changes** — Modify layout in one place instead of 27 - **Better performance** — Fewer DOM operations during settings navigation
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/anything-llm#3219
No description provided.