Skip to content

Instantly share code, notes, and snippets.

@joshcoolman-smc
Created December 3, 2024 18:13
Show Gist options
  • Select an option

  • Save joshcoolman-smc/cc418d390edc6793d5b7596b99811ac5 to your computer and use it in GitHub Desktop.

Select an option

Save joshcoolman-smc/cc418d390edc6793d5b7596b99811ac5 to your computer and use it in GitHub Desktop.

Feature Development Conventions

This document outlines our conventions for implementing features using a clean architecture approach with TypeScript, React, and Zod. The pattern described here promotes maintainability, scalability, and testability.

Feature Structure

Features should be organized in a dedicated feature folder with the following structure:

src/features/[feature-name]/
├── components/          # React components specific to the feature
├── hooks/              # Custom React hooks for feature logic
├── interfaces/         # TypeScript interfaces and types
├── repositories/       # Data access layer implementations
├── schemas/           # Zod schemas for validation and type inference
└── services/          # Business logic layer

Implementation Guidelines

1. Schema Definition

Start by defining your feature's data models using Zod schemas. This provides:

  • Type inference
  • Runtime validation
  • Clear data structure documentation

Example:

// schemas/[feature].schema.ts
import { z } from "zod";

export const entitySchema = z.object({
  id: z.string(),
  // ... other fields
  createdAt: z.date()
});

export const createEntitySchema = entitySchema.omit({
  id: true,
  createdAt: true
});

export type Entity = z.infer<typeof entitySchema>;
export type CreateEntityInput = z.infer<typeof createEntitySchema>;

2. Repository Pattern

Define a repository interface and implementation for data access:

  1. Create the interface:
// interfaces/IRepository.ts
export interface IRepository<T> {
  getAll(): Promise<T[]>;
  getById(id: string): Promise<T | null>;
  create(data: CreateInput): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}
  1. Implement the repository:
// repositories/Repository.ts
export class Repository implements IRepository<T> {
  // Implement methods with actual data access logic
  // Could be API calls, localStorage, etc.
}

3. Service Layer

Create a service layer to handle business logic:

  1. Define the service interface:
// interfaces/IService.ts
export interface IService {
  // Define business operations
  // These should be high-level use cases
}
  1. Implement the service:
// services/Service.ts
export class Service implements IService {
  constructor(private repository: IRepository) {}
  
  // Implement business logic
  // Coordinate between repository and other services
  // Handle complex operations
}

4. Custom Hooks

Create React hooks to:

  • Connect UI components with services
  • Manage state
  • Handle side effects
  • Provide error handling
// hooks/useFeature.ts
export function useFeature() {
  const [data, setData] = useState<Data[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  // Implement feature operations
  // Handle errors
  // Manage loading states
  
  return {
    data,
    error,
    isLoading,
    // ... operations
  };
}

5. React Components

Create components that:

  • Use the custom hooks for data and operations
  • Focus purely on UI concerns
  • Handle user interactions
  • Manage local UI state only
// components/FeatureComponent.tsx
export function FeatureComponent() {
  const { data, error, isLoading, operations } = useFeature();
  
  // Implement UI logic
  // Handle user interactions
  // Render UI elements
}

Best Practices

  1. Separation of Concerns

    • Repositories handle data access
    • Services handle business logic
    • Hooks handle state management and service coordination
    • Components handle UI rendering and user interaction
  2. Type Safety

    • Use Zod schemas for runtime validation
    • Leverage TypeScript for compile-time type checking
    • Export types from schemas for consistency
  3. Error Handling

    • Handle errors at appropriate levels
    • Provide meaningful error messages
    • Use error boundaries where appropriate
  4. State Management

    • Keep state as close to where it's needed as possible
    • Use custom hooks to encapsulate complex state logic
    • Consider performance implications of state updates
  5. Testing

    • Repository methods should be easily testable
    • Services should be pure and testable
    • Hooks should be tested with React Testing Library
    • Components should focus on UI testing

Example Implementation

The Todo feature in this project serves as a reference implementation of these conventions. Key aspects include:

  • Zod schemas for todo item validation
  • Repository pattern for data persistence
  • Service layer for business logic
  • Custom hook (useTodos) for state management
  • Clean component implementation

Benefits

This pattern provides:

  • Clear separation of concerns
  • Type safety throughout the application
  • Consistent error handling
  • Testable code
  • Maintainable and scalable features
  • Easy to understand and modify
  • Reusable components and logic

When to Create a Feature

Create a new feature when you have:

  • A distinct set of related functionality
  • Unique data models
  • Specific business logic
  • Dedicated UI components

Each feature should be self-contained but able to interact with other features through well-defined interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment