Skip to main content

Feature Development Guide

Feature Structure

Organize features with clear boundaries and consistent structure:
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
Create clean public APIs through index files:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
/**
 * 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:
// 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';
}