Feature Development Guide
Feature Structure
Organize features with clear boundaries and consistent structure:Copy
src/features/[feature-name]/
├── index.tsx # Public API exports
├── components/ # Feature-specific components
│ ├── FeatureList.tsx
│ ├── FeatureItem.tsx
│ └── CreateFeatureDialog.tsx
├── hooks/ # Feature-specific hooks
│ ├── useFeatureData.ts
│ └── useFeatureActions.ts
├── utils/ # Feature-specific utilities
│ ├── validation.ts
│ └── helpers.ts
├── types.ts # Feature-specific types
└── constants.ts # Feature-specific constants
Copy
// src/features/task/index.tsx
export { TaskList } from './components/TaskList';
export { TaskItem } from './components/TaskItem';
export { CreateTaskDialog } from './components/CreateTaskDialog';
export { useTask, useTaskActions } from './hooks/useTask';
export { useTaskFilters } from './hooks/useTaskFilters';
export type {
Task,
TaskInput,
TaskFilters
} from './types';
export {
TASK_STATUSES,
TASK_PRIORITIES
} from './constants';
Feature-Specific Hooks
Encapsulate feature logic in custom hooks:Copy
// src/features/task/hooks/useTaskActions.ts
export function useTaskActions(teamId: string) {
const createTask = useCallback(async (taskData: CreateTaskInput) => {
const taskId = id();
await db.transact(
db.tx.tasks[taskId].update({
...taskData,
teamId,
createdAt: new Date()
})
);
return taskId;
}, [teamId]);
const updateTask = useCallback(async (
taskId: string,
updates: Partial<Task>
) => {
await db.transact(
db.tx.tasks[taskId].update({
...updates,
updatedAt: new Date()
})
);
}, []);
const deleteTask = useCallback(async (taskId: string) => {
await db.transact(
db.tx.tasks[taskId].update({
deletedAt: new Date()
})
);
}, []);
const moveTask = useCallback(async (
taskId: string,
columnId: string
) => {
await db.transact([
db.tx.tasks[taskId].update({
columnId,
updatedAt: new Date()
}),
db.tx.events[id()].update({
type: 'task_moved',
payload: { taskId, columnId },
teamId,
createdAt: new Date()
})
]);
}, [teamId]);
return {
createTask,
updateTask,
deleteTask,
moveTask
};
}
// Data fetching hook with filters
export function useTaskData(teamId: string, filters: TaskFilters = {}) {
const query = useMemo(() => {
const whereClause: any = {
teamId,
deletedAt: { $isNull: true }
};
if (filters.status && filters.status !== 'all') {
whereClause.completed = filters.status === 'completed';
}
if (filters.columnId) {
whereClause.columnId = filters.columnId;
}
return {
tasks: {
$: {
where: whereClause,
order: { createdAt: 'desc' }
},
columns: {},
issues: {
$: {
where: { deletedAt: { $isNull: true } },
order: { createdAt: 'desc' }
}
}
}
};
}, [teamId, filters]);
return db.useQuery(query);
}
Component Patterns
Implement consistent component patterns within features:Copy
// src/features/task/components/TaskList.tsx
interface TaskListProps {
columnId?: string;
filters?: TaskFilters;
isEditable?: boolean;
onTaskSelect?: (task: Task) => void;
}
export function TaskList({
columnId,
filters = {},
isEditable = false,
onTaskSelect
}: TaskListProps) {
const teamId = useTeamContext();
const { data, isLoading, error } = useTaskData(teamId, {
...filters,
columnId
});
const { updateTask, deleteTask } = useTaskActions(teamId);
const handleTaskUpdate = useCallback(async (
taskId: string,
updates: Partial<Task>
) => {
try {
await updateTask(taskId, updates);
toast.success('Task updated successfully');
} catch (error) {
toast.error('Failed to update task');
}
}, [updateTask]);
if (isLoading) {
return (
<VStack spacing={2}>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} height="60px" width="100%" />
))}
</VStack>
);
}
if (error) {
return (
<Alert status="error">
<AlertIcon />
Failed to load tasks: {error.message}
</Alert>
);
}
const tasks = data?.tasks || [];
if (tasks.length === 0) {
return (
<EmptyState
icon={<FaTasks />}
title="No tasks found"
description={
columnId
? "This column doesn't have any tasks yet"
: "Create your first task to get started"
}
/>
);
}
return (
<VStack spacing={2} align="stretch">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
isEditable={isEditable}
onUpdate={handleTaskUpdate}
onDelete={() => deleteTask(task.id)}
onClick={() => onTaskSelect?.(task)}
/>
))}
</VStack>
);
}
Feature Integration
Handle cross-feature communication through shared patterns:Copy
// src/features/task/hooks/useTaskIntegration.ts
export function useTaskIntegration() {
// Integration with board feature
const { currentBoardId } = useBoardContext();
// Integration with events for audit logging
const { logEvent } = useEventTracking();
// Integration with AI for smart suggestions
const { analyzeTask } = useAI();
const createTaskWithIntegration = useCallback(async (
taskData: CreateTaskInput
) => {
const taskId = id();
// Create task
await db.transact(
db.tx.tasks[taskId].update({
...taskData,
boardId: currentBoardId,
createdAt: new Date()
})
);
// Log event for audit trail
await logEvent({
type: 'task_created',
entityId: taskId,
entityType: 'task'
});
// Trigger AI analysis if enabled
if (taskData.enableAI) {
await analyzeTask(taskId);
}
return taskId;
}, [currentBoardId, logEvent, analyzeTask]);
return {
createTaskWithIntegration
};
}
// Event-driven communication
export function useTaskEvents() {
const { emitEvent, useEventListener } = useEventBus();
// Listen for board changes
useEventListener('board:changed', (boardId: string) => {
// Refresh task data or update filters
console.log('Board changed, updating task view:', boardId);
});
// Emit task events
const notifyTaskCreated = useCallback((task: Task) => {
emitEvent('task:created', { task });
}, [emitEvent]);
const notifyTaskUpdated = useCallback((task: Task) => {
emitEvent('task:updated', { task });
}, [emitEvent]);
return {
notifyTaskCreated,
notifyTaskUpdated
};
}
Feature Context
Provide feature-level context when needed:Copy
// src/features/task/TaskContext.tsx
interface TaskContextValue {
selectedTaskId: string | null;
setSelectedTaskId: (id: string | null) => void;
filters: TaskFilters;
setFilters: (filters: TaskFilters) => void;
isEditMode: boolean;
setIsEditMode: (enabled: boolean) => void;
}
const TaskContext = createContext<TaskContextValue | null>(null);
export function TaskProvider({ children }: { children: ReactNode }) {
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
const [filters, setFilters] = useState<TaskFilters>({ status: 'all' });
const [isEditMode, setIsEditMode] = useState(false);
const value = useMemo(() => ({
selectedTaskId,
setSelectedTaskId,
filters,
setFilters,
isEditMode,
setIsEditMode
}), [selectedTaskId, filters, isEditMode]);
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
}
export function useTaskContext() {
const context = useContext(TaskContext);
if (!context) {
throw new Error('useTaskContext must be used within TaskProvider');
}
return context;
}
// Usage in page components
function TaskPage() {
return (
<TaskProvider>
<TaskDashboard />
</TaskProvider>
);
}
Type Safety
Define comprehensive types for the feature:Copy
// src/features/task/types.ts
export interface Task {
id: string;
title: string;
content?: any; // Rich text content
completed: boolean;
priority: TaskPriority;
teamId: string;
columnId: string;
boardId?: string;
creatorId?: string;
assigneeId?: string;
dueDate?: Date;
createdAt?: Date;
updatedAt?: Date;
deletedAt?: Date;
}
export interface CreateTaskInput {
title: string;
content?: any;
priority?: TaskPriority;
columnId: string;
assigneeId?: string;
dueDate?: Date;
}
export interface UpdateTaskInput extends Partial<Omit<Task, 'id' | 'createdAt'>> {}
export interface TaskFilters {
status?: 'all' | 'completed' | 'pending';
priority?: TaskPriority;
assigneeId?: string;
columnId?: string;
search?: string;
}
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
export interface TaskStats {
total: number;
completed: number;
pending: number;
overdue: number;
}
// Event types for integration
export interface TaskCreatedEvent {
type: 'task:created';
payload: { task: Task };
}
export interface TaskUpdatedEvent {
type: 'task:updated';
payload: {
task: Task;
previousState: Partial<Task>;
};
}
export type TaskEvent = TaskCreatedEvent | TaskUpdatedEvent;
Testing Strategy
Implement comprehensive testing for features:Copy
// src/features/task/__tests__/useTaskActions.test.ts
import { renderHook, act } from '@testing-library/react';
import { useTaskActions } from '../hooks/useTaskActions';
const mockTransact = jest.fn();
jest.mock('@/instantdb', () => ({
db: {
transact: mockTransact
}
}));
describe('useTaskActions', () => {
beforeEach(() => {
mockTransact.mockClear();
});
test('creates task with correct data', async () => {
const { result } = renderHook(() => useTaskActions('team-1'));
await act(async () => {
await result.current.createTask({
title: 'New Task',
columnId: 'column-1'
});
});
expect(mockTransact).toHaveBeenCalledWith(
expect.objectContaining({
tasks: expect.objectContaining({
[expect.any(String)]: expect.objectContaining({
update: expect.objectContaining({
title: 'New Task',
columnId: 'column-1',
teamId: 'team-1'
})
})
})
})
);
});
test('updates task with proper timestamp', async () => {
const { result } = renderHook(() => useTaskActions('team-1'));
const taskId = 'task-1';
await act(async () => {
await result.current.updateTask(taskId, {
completed: true
});
});
expect(mockTransact).toHaveBeenCalledWith(
expect.objectContaining({
tasks: expect.objectContaining({
[taskId]: expect.objectContaining({
update: expect.objectContaining({
completed: true,
updatedAt: expect.any(Date)
})
})
})
})
);
});
});
Feature Documentation
Document complex features thoroughly:Copy
/**
* Task Management Feature
*
* Provides comprehensive task management capabilities including:
* - CRUD operations for tasks
* - Real-time collaboration
* - Task filtering and search
* - Integration with boards and columns
* - Issue tracking and discussions
*
* @example
* ```tsx
* import { TaskList, useTaskActions } from '@/features/task';
*
* function MyComponent() {
* const { createTask } = useTaskActions('team-1');
*
* return (
* <div>
* <TaskList columnId="column-1" isEditable />
* <button onClick={() => createTask({ title: 'New Task' })}>
* Add Task
* </button>
* </div>
* );
* }
* ```
*/
// API Documentation
export interface TaskAPI {
/**
* Creates a new task with the provided data
* @param taskData - Task creation data
* @returns Promise resolving to the created task ID
* @throws Error if creation fails
*/
createTask(taskData: CreateTaskInput): Promise<string>;
/**
* Updates an existing task
* @param taskId - ID of the task to update
* @param updates - Partial task data to update
* @throws Error if task doesn't exist or update fails
*/
updateTask(taskId: string, updates: UpdateTaskInput): Promise<void>;
/**
* Soft deletes a task by setting deletedAt timestamp
* @param taskId - ID of the task to delete
* @throws Error if task doesn't exist
*/
deleteTask(taskId: string): Promise<void>;
}
Migration Patterns
Handle feature evolution and migrations:Copy
// src/features/task/migrations/taskMigrations.ts
export const TASK_MIGRATIONS = {
'1.0.0': {
description: 'Initial task schema',
// Initial implementation
},
'1.1.0': {
description: 'Added priority and due date fields',
migrate: (task: any) => ({
...task,
priority: task.priority || 'medium',
dueDate: task.dueDate || null
})
},
'1.2.0': {
description: 'Added assignee support',
migrate: (task: any) => ({
...task,
assigneeId: task.assigneeId || null
})
}
};
// Feature versioning
export const TASK_FEATURE_VERSION = '1.2.0';
// Backward compatibility checks
export function isTaskCompatible(task: any): boolean {
return task && typeof task.id === 'string' && typeof task.title === 'string';
}