Skip to content

Commit 11f75e6

Browse files
committed
feat: refactor project and devlog components to use useCallback for improved performance
1 parent 89b500d commit 11f75e6

File tree

5 files changed

+112
-73
lines changed

5 files changed

+112
-73
lines changed

apps/web/app/projects/[name]/settings/project-settings-page.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useEffect, useState } from 'react';
3+
import React, { useEffect, useState, useCallback } from 'react';
44
import { useProjectStore } from '@/stores';
55
import { useRouter } from 'next/navigation';
66
import { Button } from '@/components/ui/button';
@@ -79,7 +79,7 @@ export function ProjectSettingsPage() {
7979
fetchCurrentProject();
8080
}, [currentProjectName]);
8181

82-
const handleUpdateProject = async (e: React.FormEvent) => {
82+
const handleUpdateProject = useCallback(async (e: React.FormEvent) => {
8383
e.preventDefault();
8484

8585
if (!formData.name.trim()) {
@@ -109,9 +109,9 @@ export function ProjectSettingsPage() {
109109
} finally {
110110
setIsUpdating(false);
111111
}
112-
};
112+
}, [formData, project, updateProject]);
113113

114-
const handleDeleteProject = async () => {
114+
const handleDeleteProject = useCallback(async () => {
115115
if (!project) {
116116
toast.error('Project not found');
117117
return;
@@ -130,17 +130,22 @@ export function ProjectSettingsPage() {
130130
} finally {
131131
setIsDeleting(false);
132132
}
133-
};
133+
}, [project, deleteProject, router]);
134134

135-
const handleResetForm = () => {
135+
const handleResetForm = useCallback(() => {
136136
if (project) {
137137
setFormData({
138138
name: project.name,
139139
description: project.description || '',
140140
});
141141
setHasChanges(false);
142142
}
143-
};
143+
}, [project]);
144+
145+
const handleFormChange = useCallback((field: keyof ProjectFormData, value: string) => {
146+
setFormData(prev => ({ ...prev, [field]: value }));
147+
setHasChanges(true);
148+
}, []);
144149

145150
if (currentProjectContext.loading || !project) {
146151
return (
@@ -250,7 +255,7 @@ export function ProjectSettingsPage() {
250255
id="name"
251256
placeholder="e.g., My Development Project"
252257
value={formData.name}
253-
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
258+
onChange={(e) => handleFormChange('name', e.target.value)}
254259
required
255260
/>
256261
</div>
@@ -261,7 +266,7 @@ export function ProjectSettingsPage() {
261266
id="description"
262267
placeholder="Describe what this project is about..."
263268
value={formData.description || ''}
264-
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
269+
onChange={(e) => handleFormChange('description', e.target.value)}
265270
rows={3}
266271
/>
267272
</div>

apps/web/app/projects/project-list-page.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useEffect, useState } from 'react';
3+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
44
import { useProjectStore, useRealtimeStore } from '@/stores';
55
import { useRouter } from 'next/navigation';
66
import { ProjectGridSkeleton } from '@/components/common';
@@ -57,7 +57,7 @@ export function ProjectListPage() {
5757

5858
const { data: projects, loading: isLoadingProjects } = projectsContext;
5959

60-
const handleCreateProject = async (e: React.FormEvent) => {
60+
const handleCreateProject = useCallback(async (e: React.FormEvent) => {
6161
e.preventDefault();
6262

6363
if (!formData.name.trim()) {
@@ -80,16 +80,29 @@ export function ProjectListPage() {
8080
} finally {
8181
setCreating(false);
8282
}
83-
};
83+
}, [formData, fetchProjects]);
8484

85-
const handleViewProject = (projectName: string) => {
85+
const handleViewProject = useCallback((projectName: string) => {
8686
router.push(`/projects/${projectName}`);
87-
};
87+
}, [router]);
8888

89-
const handleProjectSettings = (e: React.MouseEvent, projectName: string) => {
89+
const handleProjectSettings = useCallback((e: React.MouseEvent, projectName: string) => {
9090
e.stopPropagation(); // Prevent card click from triggering
9191
router.push(`/projects/${projectName}/settings`);
92-
};
92+
}, [router]);
93+
94+
const handleOpenModal = useCallback(() => {
95+
setIsModalVisible(true);
96+
}, []);
97+
98+
const handleCloseModal = useCallback(() => {
99+
setIsModalVisible(false);
100+
setFormData({ name: '', description: '' });
101+
}, []);
102+
103+
const handleFormChange = useCallback((field: keyof ProjectFormData, value: string) => {
104+
setFormData(prev => ({ ...prev, [field]: value }));
105+
}, []);
93106

94107
if (projectsContext.error) {
95108
return (
@@ -108,7 +121,7 @@ export function ProjectListPage() {
108121
<div className="w-full max-w-full p-6">
109122
<div className="max-w-7xl mx-auto">
110123
<div className="flex items-center gap-4 mb-6">
111-
<Button className="bg-primary" onClick={() => setIsModalVisible(true)}>
124+
<Button className="bg-primary" onClick={handleOpenModal}>
112125
New Project
113126
</Button>
114127
<div className="relative">
@@ -177,7 +190,7 @@ export function ProjectListPage() {
177190
</p>
178191
<Button
179192
size="lg"
180-
onClick={() => setIsModalVisible(true)}
193+
onClick={handleOpenModal}
181194
className="flex items-center gap-2 px-8 py-3"
182195
>
183196
<Plus size={18} />
@@ -203,7 +216,7 @@ export function ProjectListPage() {
203216
id="name"
204217
placeholder="e.g., My-Dev-Project_2025"
205218
value={formData.name}
206-
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
219+
onChange={(e) => handleFormChange('name', e.target.value)}
207220
required
208221
/>
209222
<p className="text-sm text-muted-foreground mt-1">
@@ -216,18 +229,15 @@ export function ProjectListPage() {
216229
id="description"
217230
placeholder="Describe what this project is about..."
218231
value={formData.description || ''}
219-
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
232+
onChange={(e) => handleFormChange('description', e.target.value)}
220233
rows={3}
221234
/>
222235
</div>
223236
<DialogFooter>
224237
<Button
225238
type="button"
226239
variant="outline"
227-
onClick={() => {
228-
setIsModalVisible(false);
229-
setFormData({ name: '', description: '' });
230-
}}
240+
onClick={handleCloseModal}
231241
>
232242
Cancel
233243
</Button>

apps/web/components/common/overview-stats/overview-stats.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React from 'react';
3+
import React, { useCallback, useMemo } from 'react';
44
import { BarChart3 } from 'lucide-react';
55
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
66
import { Skeleton } from '@/components/ui/skeleton';
@@ -52,32 +52,32 @@ export function OverviewStats({
5252
return null;
5353
}
5454

55-
const isStatusActive = (status: DevlogStatus) => {
55+
const isStatusActive = useCallback((status: DevlogStatus) => {
5656
return !!currentFilters?.status?.includes(status);
57-
};
57+
}, [currentFilters]);
5858

59-
const isTotalActive = () => {
59+
const isTotalActive = useCallback(() => {
6060
return (
6161
(!currentFilters?.filterType || currentFilters.filterType === 'total') &&
6262
(!currentFilters?.status || currentFilters.status.length === 0)
6363
);
64-
};
64+
}, [currentFilters]);
6565

66-
const isOpenActive = () => {
66+
const isOpenActive = useCallback(() => {
6767
return currentFilters?.filterType === 'open';
68-
};
68+
}, [currentFilters]);
6969

70-
const isClosedActive = () => {
70+
const isClosedActive = useCallback(() => {
7171
return currentFilters?.filterType === 'closed';
72-
};
72+
}, [currentFilters]);
7373

74-
const handleStatClick = (status: FilterType) => {
74+
const handleStatClick = useCallback((status: FilterType) => {
7575
if (onFilterToggle) {
7676
onFilterToggle(status);
7777
}
78-
};
78+
}, [onFilterToggle]);
7979

80-
const getStatClasses = (filterType: FilterType, isIndividualStatus = false) => {
80+
const getStatClasses = useCallback((filterType: FilterType, isIndividualStatus = false) => {
8181
let isActive: boolean;
8282
if (filterType === 'total') {
8383
isActive = isTotalActive();
@@ -99,9 +99,9 @@ export function OverviewStats({
9999
'hover:bg-muted': isClickable && !isActive,
100100
},
101101
);
102-
};
102+
}, [isTotalActive, isOpenActive, isClosedActive, isStatusActive, onFilterToggle]);
103103

104-
const getStatusColor = (status: DevlogStatus) => {
104+
const getStatusColor = useCallback((status: DevlogStatus) => {
105105
const colors = {
106106
new: 'text-blue-600',
107107
'in-progress': 'text-orange-600',
@@ -112,7 +112,7 @@ export function OverviewStats({
112112
cancelled: 'text-gray-600',
113113
};
114114
return colors[status] || 'text-foreground';
115-
};
115+
}, []);
116116

117117
const StatItem = ({
118118
value,

apps/web/components/custom/editable-field.tsx

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import React, { useEffect, useRef, useState } from 'react';
3+
import React, { useEffect, useRef, useState, useCallback } from 'react';
44
import { Input } from '@/components/ui/input';
55
import { Textarea } from '@/components/ui/textarea';
66
import {
@@ -66,30 +66,26 @@ export function EditableField({
6666
}
6767
}, [isEditing]);
6868

69-
const handleSave = () => {
69+
const handleSave = useCallback(() => {
7070
onSave(editValue);
7171
setIsEditing(false);
72-
};
72+
}, [editValue, onSave]);
7373

74-
const handleCancel = () => {
74+
const handleCancel = useCallback(() => {
7575
setEditValue(value);
7676
setIsEditing(false);
77-
};
77+
}, [value]);
7878

79-
const handleKeyPress = (e: React.KeyboardEvent) => {
79+
const handleKeyPress = useCallback((e: React.KeyboardEvent) => {
8080
if (e.key === 'Enter' && !multiline && type !== 'textarea') {
8181
e.preventDefault();
8282
handleSave();
8383
} else if (e.key === 'Escape') {
8484
handleCancel();
8585
}
86-
};
87-
88-
const handleBlur = () => {
89-
handleBlurWithValue(editValue);
90-
};
86+
}, [multiline, type, handleSave, handleCancel]);
9187

92-
const handleBlurWithValue = (currentValue: string) => {
88+
const handleBlurWithValue = useCallback((currentValue: string) => {
9389
if (draftMode) {
9490
// In draft mode, just save the local value and exit edit mode
9591
// The parent component will handle when to actually save
@@ -101,20 +97,44 @@ export function EditableField({
10197
// Original behavior: save changes when losing focus
10298
handleSave();
10399
}
104-
};
100+
}, [draftMode, value, onSave, handleSave]);
101+
102+
const handleBlur = useCallback(() => {
103+
handleBlurWithValue(editValue);
104+
}, [editValue, handleBlurWithValue]);
105105

106-
const handleEnterEdit = () => {
106+
const handleEnterEdit = useCallback(() => {
107107
setIsEditing(true);
108-
};
108+
}, []);
109+
110+
const handleEditValueChange = useCallback((newValue: string) => {
111+
setEditValue(newValue);
112+
}, []);
113+
114+
const handleSelectValueChange = useCallback((newValue: string) => {
115+
setEditValue(newValue);
116+
if (draftMode) {
117+
if (newValue !== value) {
118+
onSave(newValue);
119+
}
120+
setIsEditing(false);
121+
}
122+
}, [draftMode, value, onSave]);
123+
124+
const handleMouseEnter = useCallback(() => {
125+
setIsHovered(true);
126+
}, []);
127+
128+
const handleMouseLeave = useCallback(() => {
129+
setIsHovered(false);
130+
}, []);
109131

110132
const renderInput = () => {
111133
if (type === 'markdown') {
112134
return (
113135
<MarkdownEditor
114136
value={editValue || ''}
115-
onChange={(value) => {
116-
setEditValue(value);
117-
}}
137+
onChange={handleEditValueChange}
118138
onBlur={handleBlurWithValue}
119139
onCancel={handleCancel}
120140
placeholder={placeholder}
@@ -127,15 +147,7 @@ export function EditableField({
127147
return (
128148
<Select
129149
value={editValue}
130-
onValueChange={(newValue) => {
131-
setEditValue(newValue);
132-
if (draftMode) {
133-
if (newValue !== value) {
134-
onSave(newValue);
135-
}
136-
setIsEditing(false);
137-
}
138-
}}
150+
onValueChange={handleSelectValueChange}
139151
open={isEditing}
140152
onOpenChange={setIsEditing}
141153
>
@@ -163,7 +175,7 @@ export function EditableField({
163175
<Textarea
164176
ref={textareaRef}
165177
value={editValue}
166-
onChange={(e) => setEditValue(e.target.value)}
178+
onChange={(e) => handleEditValueChange(e.target.value)}
167179
onKeyDown={handleKeyPress}
168180
onBlur={handleBlur}
169181
placeholder={placeholder}
@@ -178,7 +190,7 @@ export function EditableField({
178190
<Input
179191
ref={inputRef}
180192
value={editValue}
181-
onChange={(e) => setEditValue(e.target.value)}
193+
onChange={(e) => handleEditValueChange(e.target.value)}
182194
onKeyDown={handleKeyPress}
183195
onBlur={handleBlur}
184196
placeholder={placeholder}
@@ -216,8 +228,8 @@ export function EditableField({
216228
className,
217229
)}
218230
onClick={handleEnterEdit}
219-
onMouseEnter={() => setIsHovered(true)}
220-
onMouseLeave={() => setIsHovered(false)}
231+
onMouseEnter={handleMouseEnter}
232+
onMouseLeave={handleMouseLeave}
221233
title="Click to edit"
222234
>
223235
{renderContent()}

0 commit comments

Comments
 (0)