Skip to content

Commit 90ddc4e

Browse files
committed
🤖 Eliminate prop drilling with AppContext
Replace 11-prop interface (AppInnerProps) with React Context pattern: - Created AppContext to provide app-level state (projects, workspaces, selection) - Updated AppLoader to wrap App with AppProvider - Refactored App.tsx to use useApp() hook instead of props - Updated LeftSidebar to consume context directly Benefits: - Decoupling: Components access data directly without parent involvement - Scalability: Adding state touches 2 files instead of 3+ - Consistency: Matches existing patterns (ModeContext, ThinkingContext) - Readability: No verbose prop interfaces, clear separation of concerns Net: +96 insertions, -74 deletions
1 parent 3dde873 commit 90ddc4e

File tree

4 files changed

+96
-74
lines changed

4 files changed

+96
-74
lines changed

src/App.tsx

Lines changed: 18 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import {
2-
useState,
3-
useEffect,
4-
useCallback,
5-
useRef,
6-
type Dispatch,
7-
type SetStateAction,
8-
} from "react";
1+
import { useState, useEffect, useCallback, useRef } from "react";
92
import "./styles/globals.css";
10-
import type { ProjectConfig } from "./config";
3+
import { useApp } from "./contexts/AppContext";
114
import type { WorkspaceSelection } from "./components/ProjectSidebar";
125
import type { FrontendWorkspaceMetadata } from "./types/workspace";
136
import { LeftSidebar } from "./components/LeftSidebar";
@@ -37,48 +30,20 @@ import { useTelemetry } from "./hooks/useTelemetry";
3730

3831
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3932

40-
interface AppInnerProps {
41-
projects: Map<string, ProjectConfig>;
42-
setProjects: Dispatch<SetStateAction<Map<string, ProjectConfig>>>;
43-
addProject: () => Promise<void>;
44-
removeProject: (path: string) => Promise<void>;
45-
workspaceMetadata: Map<string, FrontendWorkspaceMetadata>;
46-
setWorkspaceMetadata: Dispatch<SetStateAction<Map<string, FrontendWorkspaceMetadata>>>;
47-
createWorkspace: (
48-
projectPath: string,
49-
branchName: string,
50-
trunkBranch: string
51-
) => Promise<{
52-
projectPath: string;
53-
projectName: string;
54-
namedWorkspacePath: string;
55-
workspaceId: string;
56-
}>;
57-
removeWorkspace: (
58-
workspaceId: string,
59-
options?: { force?: boolean }
60-
) => Promise<{ success: boolean; error?: string }>;
61-
renameWorkspace: (
62-
workspaceId: string,
63-
newName: string
64-
) => Promise<{ success: boolean; error?: string }>;
65-
selectedWorkspace: WorkspaceSelection | null;
66-
setSelectedWorkspace: (workspace: WorkspaceSelection | null) => void;
67-
}
68-
69-
function AppInner({
70-
projects,
71-
setProjects: _setProjects,
72-
addProject,
73-
removeProject,
74-
workspaceMetadata,
75-
setWorkspaceMetadata,
76-
createWorkspace,
77-
removeWorkspace,
78-
renameWorkspace,
79-
selectedWorkspace,
80-
setSelectedWorkspace,
81-
}: AppInnerProps) {
33+
function AppInner() {
34+
// Get app-level state from context
35+
const {
36+
projects,
37+
addProject,
38+
removeProject,
39+
workspaceMetadata,
40+
setWorkspaceMetadata,
41+
createWorkspace,
42+
removeWorkspace,
43+
renameWorkspace,
44+
selectedWorkspace,
45+
setSelectedWorkspace,
46+
} = useApp();
8247
const [workspaceModalOpen, setWorkspaceModalOpen] = useState(false);
8348
const [workspaceModalProject, setWorkspaceModalProject] = useState<string | null>(null);
8449
const [workspaceModalProjectName, setWorkspaceModalProjectName] = useState<string>("");
@@ -642,15 +607,10 @@ function AppInner({
642607
<>
643608
<div className="bg-bg-dark flex h-screen overflow-hidden [@media(max-width:768px)]:flex-col">
644609
<LeftSidebar
645-
projects={projects}
646-
workspaceMetadata={workspaceMetadata}
647-
selectedWorkspace={selectedWorkspace}
648610
onSelectWorkspace={handleWorkspaceSwitch}
649611
onAddProject={handleAddProjectCallback}
650612
onAddWorkspace={handleAddWorkspaceCallback}
651613
onRemoveProject={handleRemoveProjectCallback}
652-
onRemoveWorkspace={removeWorkspace}
653-
onRenameWorkspace={renameWorkspace}
654614
lastReadTimestamps={lastReadTimestamps}
655615
onToggleUnread={onToggleUnread}
656616
collapsed={sidebarCollapsed}
@@ -724,10 +684,10 @@ function AppInner({
724684
);
725685
}
726686

727-
function App(props: AppInnerProps) {
687+
function App() {
728688
return (
729689
<CommandRegistryProvider>
730-
<AppInner {...props} />
690+
<AppInner />
731691
</CommandRegistryProvider>
732692
);
733693
}

src/components/AppLoader.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useWorkspaceStoreRaw } from "../stores/WorkspaceStore";
77
import { useGitStatusStoreRaw } from "../stores/GitStatusStore";
88
import { usePersistedState } from "../hooks/usePersistedState";
99
import type { WorkspaceSelection } from "./ProjectSidebar";
10+
import { AppProvider } from "../contexts/AppContext";
1011

1112
/**
1213
* AppLoader handles all initialization before rendering the main App:
@@ -100,9 +101,9 @@ export function AppLoader() {
100101
return <LoadingScreen />;
101102
}
102103

103-
// Render App with all initialized data
104+
// Render App with all initialized data via context
104105
return (
105-
<App
106+
<AppProvider
106107
projects={projectManagement.projects}
107108
setProjects={projectManagement.setProjects}
108109
addProject={projectManagement.addProject}
@@ -114,6 +115,8 @@ export function AppLoader() {
114115
renameWorkspace={workspaceManagement.renameWorkspace}
115116
selectedWorkspace={selectedWorkspace}
116117
setSelectedWorkspace={setSelectedWorkspace}
117-
/>
118+
>
119+
<App />
120+
</AppProvider>
118121
);
119122
}

src/components/LeftSidebar.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import React from "react";
22
import { cn } from "@/lib/utils";
3-
import type { ProjectConfig } from "@/config";
43
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
5-
import type { WorkspaceSelection } from "./ProjectSidebar";
64
import type { Secret } from "@/types/secrets";
75
import ProjectSidebar from "./ProjectSidebar";
86
import { TitleBar } from "./TitleBar";
7+
import { useApp } from "@/contexts/AppContext";
8+
import type { WorkspaceSelection } from "./ProjectSidebar";
99

1010
interface LeftSidebarProps {
11-
projects: Map<string, ProjectConfig>;
12-
workspaceMetadata: Map<string, FrontendWorkspaceMetadata>;
13-
selectedWorkspace: WorkspaceSelection | null;
1411
onSelectWorkspace: (selection: WorkspaceSelection) => void;
1512
onAddProject: () => void;
1613
onAddWorkspace: (projectPath: string) => void;
1714
onRemoveProject: (path: string) => void;
18-
onRemoveWorkspace: (
19-
workspaceId: string,
20-
options?: { force?: boolean }
21-
) => Promise<{ success: boolean; error?: string }>;
22-
onRenameWorkspace: (
23-
workspaceId: string,
24-
newName: string
25-
) => Promise<{ success: boolean; error?: string }>;
2615
lastReadTimestamps: Record<string, number>;
2716
onToggleUnread: (workspaceId: string) => void;
2817
collapsed: boolean;
@@ -36,6 +25,10 @@ interface LeftSidebarProps {
3625
export function LeftSidebar(props: LeftSidebarProps) {
3726
const { collapsed, onToggleCollapsed, ...projectSidebarProps } = props;
3827

28+
// Get app-level state from context
29+
const { projects, workspaceMetadata, selectedWorkspace, removeWorkspace, renameWorkspace } =
30+
useApp();
31+
3932
return (
4033
<>
4134
{/* Hamburger menu button - only visible on mobile */}
@@ -82,6 +75,11 @@ export function LeftSidebar(props: LeftSidebarProps) {
8275
{!collapsed && <TitleBar />}
8376
<ProjectSidebar
8477
{...projectSidebarProps}
78+
projects={projects}
79+
workspaceMetadata={workspaceMetadata}
80+
selectedWorkspace={selectedWorkspace}
81+
onRemoveWorkspace={removeWorkspace}
82+
onRenameWorkspace={renameWorkspace}
8583
collapsed={collapsed}
8684
onToggleCollapsed={onToggleCollapsed}
8785
/>

src/contexts/AppContext.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ReactNode, Dispatch, SetStateAction } from "react";
2+
import { createContext, useContext } from "react";
3+
import type { ProjectConfig } from "@/config";
4+
import type { FrontendWorkspaceMetadata } from "@/types/workspace";
5+
import type { WorkspaceSelection } from "@/components/ProjectSidebar";
6+
7+
/**
8+
* App-level state and operations shared across the component tree.
9+
* Eliminates prop drilling for common data like projects, workspaces, and selection.
10+
*/
11+
interface AppContextType {
12+
// Projects
13+
projects: Map<string, ProjectConfig>;
14+
setProjects: Dispatch<SetStateAction<Map<string, ProjectConfig>>>;
15+
addProject: () => Promise<void>;
16+
removeProject: (path: string) => Promise<void>;
17+
18+
// Workspaces
19+
workspaceMetadata: Map<string, FrontendWorkspaceMetadata>;
20+
setWorkspaceMetadata: Dispatch<SetStateAction<Map<string, FrontendWorkspaceMetadata>>>;
21+
createWorkspace: (
22+
projectPath: string,
23+
branchName: string,
24+
trunkBranch: string
25+
) => Promise<{
26+
projectPath: string;
27+
projectName: string;
28+
namedWorkspacePath: string;
29+
workspaceId: string;
30+
}>;
31+
removeWorkspace: (
32+
workspaceId: string,
33+
options?: { force?: boolean }
34+
) => Promise<{ success: boolean; error?: string }>;
35+
renameWorkspace: (
36+
workspaceId: string,
37+
newName: string
38+
) => Promise<{ success: boolean; error?: string }>;
39+
40+
// Selection
41+
selectedWorkspace: WorkspaceSelection | null;
42+
setSelectedWorkspace: (workspace: WorkspaceSelection | null) => void;
43+
}
44+
45+
const AppContext = createContext<AppContextType | undefined>(undefined);
46+
47+
interface AppProviderProps extends AppContextType {
48+
children: ReactNode;
49+
}
50+
51+
export const AppProvider: React.FC<AppProviderProps> = ({ children, ...value }) => {
52+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
53+
};
54+
55+
export const useApp = (): AppContextType => {
56+
const context = useContext(AppContext);
57+
if (!context) {
58+
throw new Error("useApp must be used within AppProvider");
59+
}
60+
return context;
61+
};

0 commit comments

Comments
 (0)