UTA DevHub
Coding Standards

TypeScript & React Patterns

Comprehensive coding patterns and conventions for TypeScript and React Native development

TypeScript & React Patterns

Overview

This document defines the exact patterns and conventions for TypeScript and React Native development. Every team member and AI assistant must follow these patterns to maintain our "single author" codebase consistency.

Mandatory: These patterns are not suggestions. They are required standards that must be followed in all code.

TypeScript Conventions

Type vs Interface

Rule: Use interface for object shapes, type for unions, intersections, and primitives.

// ✅ GOOD - Interface for objects
interface User {
  id: string;
  name: string;
  email: string;
}
 
interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}
 
// ✅ GOOD - Type for unions and primitives
type Status = 'idle' | 'loading' | 'success' | 'error';
type ID = string | number;
type Nullable<T> = T | null;
 
// ❌ BAD - Using type for simple objects
type User = {
  id: string;
  name: string;
};

Naming Conventions

// Interfaces: PascalCase with descriptive names
interface UserProfile { }
interface ApiClientConfig { }
interface AuthenticationState { }
 
// Types: PascalCase, descriptive with suffix for clarity
type ButtonVariant = 'primary' | 'secondary';
type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type UserId = string;
 
// Enums: PascalCase with UPPER_SNAKE_CASE values
enum UserRole {
  ADMIN = 'ADMIN',
  USER = 'USER',
  GUEST = 'GUEST'
}
 
// Generic Type Parameters: Single uppercase letters or descriptive
function map<T, R>(items: T[], fn: (item: T) => R): R[] { }
interface ApiResponse<TData, TError = Error> { }

Function Types

// ✅ GOOD - Clear function type definitions
interface UserService {
  getUser: (id: string) => Promise<User>;
  updateUser: (id: string, data: Partial<User>) => Promise<User>;
  deleteUser: (id: string) => Promise<void>;
}
 
// ✅ GOOD - Standalone function types
type Validator<T> = (value: T) => boolean;
type AsyncCallback<T> = (data: T) => Promise<void>;
 
// ❌ BAD - Unclear or overly complex inline types
interface Service {
  fetch: (id: string) => Promise<any>; // Avoid 'any'
  update: Function; // Too generic
}

Import Organization

Rule: Organize imports in this exact order:

// 1. React and React Native imports
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
 
// 2. Third-party libraries
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
 
// 3. Absolute imports from project (ordered by path depth)
import { useAuth } from '@/core/domains/auth';
import { Button } from '@/ui/foundation/Button';
import { Modal } from '@/ui/patterns/Modal';
import { ProductCard } from '@/ui/business/ProductCard';
import { api } from '@/core/api';
 
// 4. Relative imports
import { LocalComponent } from './LocalComponent';
import { styles } from './styles';
import type { LocalType } from './types';
 
// 5. Type imports (if not inline)
import type { User, Product } from '@/core/domains/types';

React Component Patterns

Functional Components

Rule: Always use function declarations for components, arrow functions for inline callbacks.

// ✅ GOOD - Function declaration for components
export function UserCard({ user, onPress }: UserCardProps) {
  return (
    <TouchableOpacity onPress={() => onPress(user.id)}>
      <Text>{user.name}</Text>
    </TouchableOpacity>
  );
}
 
// ❌ BAD - Arrow function for components
export const UserCard = ({ user, onPress }: UserCardProps) => { };

Component Structure

Rule: Follow this exact structure for all components:

// 1. Imports
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
 
// 2. Type definitions
interface ProductCardProps {
  product: Product;
  onPress?: (id: string) => void;
  variant?: 'default' | 'compact';
}
 
// 3. Component definition
export function ProductCard({ 
  product, 
  onPress,
  variant = 'default' 
}: ProductCardProps) {
  // 4. Hooks (state, context, custom)
  const [isLoaded, setIsLoaded] = useState(false);
  const { user } = useAuth();
  
  // 5. Derived state / memoized values
  const formattedPrice = useMemo(
    () => formatCurrency(product.price),
    [product.price]
  );
  
  // 6. Effects
  useEffect(() => {
    // Effect logic
  }, []);
  
  // 7. Event handlers
  const handlePress = useCallback(() => {
    onPress?.(product.id);
  }, [product.id, onPress]);
  
  // 8. Render helpers (if needed)
  const renderPrice = () => {
    return <Text>{formattedPrice}</Text>;
  };
  
  // 9. Main render
  return (
    <View style={styles.container}>
      {/* Component JSX */}
    </View>
  );
}
 
// 10. Styles (if in same file)
const styles = StyleSheet.create({
  container: {
    // styles
  }
});

Props Patterns

// ✅ GOOD - Explicit, well-typed props
interface ButtonProps {
  title: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  loading?: boolean;
  testID?: string;
}
 
// ✅ GOOD - Extending native props
interface CustomTextProps extends TextProps {
  variant?: 'heading' | 'body' | 'caption';
  color?: keyof typeof theme.colors;
}
 
// ❌ BAD - Unclear or overly permissive props
interface ButtonProps {
  [key: string]: any; // Too permissive
  onClick: Function; // Too generic
  style?: any; // Should be ViewStyle
}

Hook Patterns

Custom Hook Structure

// ✅ GOOD - Well-structured custom hook
export function useProductSearch(initialQuery = '') {
  // 1. State declarations
  const [query, setQuery] = useState(initialQuery);
  const [filters, setFilters] = useState<ProductFilters>({});
  
  // 2. Other hooks
  const debouncedQuery = useDebounce(query, 300);
  
  // 3. Queries/mutations
  const { data, isLoading, error } = useQuery({
    queryKey: ['products', 'search', debouncedQuery, filters],
    queryFn: () => searchProducts(debouncedQuery, filters),
    enabled: debouncedQuery.length > 0,
  });
  
  // 4. Callbacks/handlers
  const updateFilters = useCallback((newFilters: Partial<ProductFilters>) => {
    setFilters(prev => ({ ...prev, ...newFilters }));
  }, []);
  
  // 5. Effects
  useEffect(() => {
    // Reset filters when query changes significantly
    if (query.length === 0) {
      setFilters({});
    }
  }, [query]);
  
  // 6. Return object with consistent naming
  return {
    // State
    query,
    filters,
    products: data?.products ?? [],
    
    // Status
    isLoading,
    error,
    
    // Actions
    setQuery,
    updateFilters,
    clearFilters: () => setFilters({}),
  };
}

Hook Naming Conventions

// Data fetching hooks
useUser(id: string)           // Single item
useUsers(filters?: Filters)   // Multiple items
useCreateUser()              // Mutation
useUpdateUser()              // Mutation
useDeleteUser()              // Mutation
 
// State management hooks
useCartState()               // Local state
useAuthState()               // Global state
useFormState<T>()            // Generic state
 
// Utility hooks
useDebounce<T>(value: T, delay: number)
useLocalStorage<T>(key: string, initialValue: T)
usePrevious<T>(value: T)

State Management Patterns

Local State

// ✅ GOOD - Clear state management
function ProductList() {
  // Group related state
  const [filters, setFilters] = useState<ProductFilters>({
    category: null,
    priceRange: null,
    sortBy: 'relevance',
  });
  
  // Separate concerns
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
  const [isSelectionMode, setIsSelectionMode] = useState(false);
  
  // Update functions next to state
  const toggleSelection = (id: string) => {
    setSelectedIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };
}

Global State (Zustand)

// ✅ GOOD - Consistent store structure
interface CartStore {
  // State
  items: CartItem[];
  isLoading: boolean;
  
  // Computed
  get total(): number;
  get itemCount(): number;
  
  // Actions
  addItem: (product: Product, quantity: number) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
  
  // Async actions
  syncWithServer: () => Promise<void>;
}
 
export const useCartStore = create<CartStore>((set, get) => ({
  // Implementation
}));

Error Handling Patterns

Try-Catch Patterns

// ✅ GOOD - Consistent error handling
async function updateUserProfile(data: UpdateProfileData) {
  try {
    // Input validation first
    const validated = profileSchema.parse(data);
    
    // Main operation
    const response = await api.updateProfile(validated);
    
    // Success handling
    showToast({ type: 'success', message: 'Profile updated' });
    return response;
    
  } catch (error) {
    // Type-safe error handling
    if (error instanceof ZodError) {
      showToast({ type: 'error', message: 'Invalid data' });
      throw new ValidationError('Profile validation failed', error);
    }
    
    if (error instanceof ApiError) {
      showToast({ type: 'error', message: error.message });
      throw error;
    }
    
    // Unknown errors
    console.error('Unexpected error:', error);
    showToast({ type: 'error', message: 'Something went wrong' });
    throw new Error('Failed to update profile');
  }
}

Error Boundaries

// ✅ GOOD - Feature-level error boundary
export function ProductListScreen() {
  return (
    <ErrorBoundary
      fallback={<ErrorFallback onRetry={() => window.location.reload()} />}
      onError={(error) => {
        logError(error, { screen: 'ProductList' });
      }}
    >
      <ProductList />
    </ErrorBoundary>
  );
}

Performance Patterns

Memoization Rules

// ✅ GOOD - Appropriate memoization
function ProductCard({ product, onPress }: ProductCardProps) {
  // Memoize expensive computations
  const discountPercentage = useMemo(
    () => calculateDiscount(product.price, product.salePrice),
    [product.price, product.salePrice]
  );
  
  // Memoize callbacks passed to children
  const handlePress = useCallback(() => {
    onPress(product.id);
  }, [product.id, onPress]);
  
  // Don't memoize simple operations
  const isOnSale = product.salePrice < product.price; // ✅ Simple comparison
  
  return <View>{/* ... */}</View>;
}
 
// Component memoization for expensive renders
export const ProductCard = memo(_ProductCard, (prev, next) => {
  return prev.product.id === next.product.id &&
         prev.product.updatedAt === next.product.updatedAt;
});

List Rendering

// ✅ GOOD - Optimized list rendering
function ProductList({ products }: { products: Product[] }) {
  const renderItem = useCallback(({ item }: { item: Product }) => (
    <ProductCard
      key={item.id} // Always include key
      product={item}
      onPress={handleProductPress}
    />
  ), [handleProductPress]);
  
  const keyExtractor = useCallback((item: Product) => item.id, []);
  
  const getItemLayout = useCallback((data: any, index: number) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  }), []);
  
  return (
    <FlatList
      data={products}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      removeClippedSubviews
      maxToRenderPerBatch={10}
      windowSize={10}
    />
  );
}

Testing Patterns

Component Testing

// ✅ GOOD - Comprehensive component test
describe('ProductCard', () => {
  const mockProduct: Product = {
    id: '1',
    name: 'Test Product',
    price: 99.99,
    image: 'https://example.com/image.jpg',
  };
  
  it('renders product information correctly', () => {
    const { getByText, getByTestId } = render(
      <ProductCard product={mockProduct} />
    );
    
    expect(getByText('Test Product')).toBeTruthy();
    expect(getByText('$99.99')).toBeTruthy();
    expect(getByTestId('product-image')).toHaveProperty('source', {
      uri: mockProduct.image,
    });
  });
  
  it('calls onPress with product id when pressed', () => {
    const onPress = jest.fn();
    const { getByTestId } = render(
      <ProductCard product={mockProduct} onPress={onPress} />
    );
    
    fireEvent.press(getByTestId('product-card'));
    expect(onPress).toHaveBeenCalledWith(mockProduct.id);
  });
});

Code Comments

When to Comment

// ✅ GOOD - Explains why, not what
function calculateShippingCost(items: CartItem[], address: Address): number {
  // Free shipping for orders over $100 (business requirement BR-123)
  if (getTotalPrice(items) > 100) {
    return 0;
  }
  
  // Express shipping surcharge for specific regions due to carrier limitations
  const expressSurcharge = EXPRESS_REGIONS.includes(address.state) ? 15 : 0;
  
  return BASE_SHIPPING_COST + expressSurcharge;
}
 
// ❌ BAD - Obvious comments
function addNumbers(a: number, b: number): number {
  // Add a and b together
  return a + b; // Return the sum
}

Documentation Comments

/**
 * Processes a payment using the configured payment provider.
 * 
 * @param payment - Payment details including amount and method
 * @param options - Additional processing options
 * @returns Payment confirmation or throws PaymentError
 * 
 * @example
 * ```typescript
 * const confirmation = await processPayment({
 *   amount: 99.99,
 *   method: 'credit_card',
 *   token: 'tok_123'
 * });
 * ```
 * 
 * @throws {PaymentError} When payment fails or is declined
 * @throws {ValidationError} When payment data is invalid
 */
export async function processPayment(
  payment: PaymentRequest,
  options?: PaymentOptions
): Promise<PaymentConfirmation> {
  // Implementation
}

Common Anti-Patterns to Avoid

Never do these in our codebase:

// ❌ Using 'any' type
const data: any = fetchData(); // BAD
 
// ❌ Ignoring TypeScript errors
// @ts-ignore // NEVER DO THIS
const result = someFunction();
 
// ❌ Inconsistent naming
const user_name = 'John'; // Should be userName
const GetUserData = () => {}; // Should be getUserData
 
// ❌ Magic numbers/strings
if (status === 2) { } // What is 2?
setTimeout(fn, 3000); // Use constants
 
// ❌ Nested ternaries
const color = isActive ? 'blue' : isDisabled ? 'gray' : isError ? 'red' : 'black';
 
// ❌ Large inline functions
<Button onPress={() => {
  // 20+ lines of code here
}} />
 
// ❌ Direct state mutations
state.items.push(newItem); // Mutating state
items[0].name = 'New Name'; // Mutating props

Enforcement Checklist

Before submitting code, verify:

  • All TypeScript errors are resolved
  • Imports are organized correctly
  • Component structure follows the standard
  • Props are fully typed (no any)
  • Custom hooks start with 'use'
  • Error handling is comprehensive
  • Performance optimizations are applied where needed
  • Code comments explain 'why' not 'what'
  • No anti-patterns are present

Quick Reference

// Interface for objects
interface User { name: string; }
 
// Type for unions/primitives  
type Status = 'active' | 'inactive';
 
// Function components
export function Component() { }
 
// Custom hooks
export function useCustomHook() { }
 
// Event handlers
const handleClick = () => { };
 
// Async functions
const fetchData = async () => { };
 
// Constants
const MAX_RETRIES = 3;
const API_TIMEOUT = 5000;

API Development Decision Matrix

HTTP Method & Status Code Standards

Use this matrix for consistent API integration patterns:

ScenarioHTTP MethodStatus CodeResponse FormatError Handling
Resource CreationPOST201Created resource + location400 for validation, 409 for conflicts
Resource RetrievalGET200/404Full resource or error404 with helpful message
Resource UpdatePUT/PATCH200/204Updated resource or no content400 for validation, 404 if not found
Resource DeletionDELETE204No content404 if not found, 409 if dependencies
Batch OperationsPOST207Multi-status responseIndividual status per item
Search/FilterGET200Paginated results400 for invalid query params
File UploadPOST201File metadata + URL413 for file too large

API Function Patterns

// ✅ GOOD - Consistent API patterns
// core/domains/products/api.ts
 
// GET single resource
export const getProduct = async (id: string): Promise<Product> => {
  return apiClient.get(`/products/${id}`);
};
 
// GET collection with filters
export const getProducts = async (filters?: ProductFilters): Promise<PaginatedResponse<Product>> => {
  return apiClient.get('/products', { params: filters });
};
 
// CREATE resource
export const createProduct = async (data: CreateProductDTO): Promise<Product> => {
  return apiClient.post('/products', data);
};
 
// UPDATE resource (full)
export const updateProduct = async (id: string, data: UpdateProductDTO): Promise<Product> => {
  return apiClient.put(`/products/${id}`, data);
};
 
// UPDATE resource (partial)
export const patchProduct = async (id: string, data: Partial<UpdateProductDTO>): Promise<Product> => {
  return apiClient.patch(`/products/${id}`, data);
};
 
// DELETE resource
export const deleteProduct = async (id: string): Promise<void> => {
  return apiClient.delete(`/products/${id}`);
};
 
// BATCH operations
export const batchUpdateProducts = async (updates: BatchUpdate[]): Promise<BatchResponse> => {
  return apiClient.post('/products/batch', { operations: updates });
};

React Query Integration Patterns

// ✅ GOOD - Consistent hook patterns
// core/domains/products/hooks.ts
 
// GET hooks
export function useProduct(id: string, options?: UseQueryOptions<Product>) {
  return useQuery({
    queryKey: productQueryKeys.detail(id),
    queryFn: () => getProduct(id),
    staleTime: 5 * 60 * 1000, // 5 minutes
    ...options,
  });
}
 
export function useProducts(filters?: ProductFilters) {
  return useQuery({
    queryKey: productQueryKeys.list(filters),
    queryFn: () => getProducts(filters),
    staleTime: 1 * 60 * 1000, // 1 minute for lists
  });
}
 
// CREATE mutation
export function useCreateProduct() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: createProduct,
    onSuccess: (newProduct) => {
      // Invalidate and refetch lists
      queryClient.invalidateQueries({ 
        queryKey: productQueryKeys.lists() 
      });
      // Add to cache immediately
      queryClient.setQueryData(
        productQueryKeys.detail(newProduct.id),
        newProduct
      );
      showToast({ type: 'success', message: 'Product created successfully' });
    },
    onError: (error: ApiError) => {
      if (error.status === 409) {
        showToast({ type: 'error', message: 'Product already exists' });
      } else {
        showToast({ type: 'error', message: error.message });
      }
    },
  });
}
 
// UPDATE mutation
export function useUpdateProduct() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateProductDTO }) =>
      updateProduct(id, data),
    onSuccess: (updatedProduct) => {
      // Update cache
      queryClient.setQueryData(
        productQueryKeys.detail(updatedProduct.id),
        updatedProduct
      );
      // Invalidate lists
      queryClient.invalidateQueries({ 
        queryKey: productQueryKeys.lists() 
      });
    },
  });
}
 
// DELETE mutation
export function useDeleteProduct() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: deleteProduct,
    onSuccess: (_, deletedId) => {
      // Remove from cache
      queryClient.removeQueries({
        queryKey: productQueryKeys.detail(deletedId),
      });
      // Invalidate lists
      queryClient.invalidateQueries({ 
        queryKey: productQueryKeys.lists() 
      });
    },
  });
}

Error Response Handling

// ✅ GOOD - Standardized error responses
interface ApiErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Record<string, any>;
    timestamp: string;
    path: string;
  };
}
 
// Error handler in API client
const handleApiError = (error: AxiosError<ApiErrorResponse>): never => {
  if (error.response) {
    const { status, data } = error.response;
    
    switch (status) {
      case 400:
        throw new ValidationError(data.error.message, data.error.details);
      case 401:
        throw new AuthenticationError('Please login to continue');
      case 403:
        throw new AuthorizationError('You do not have permission');
      case 404:
        throw new NotFoundError(data.error.message);
      case 409:
        throw new ConflictError(data.error.message);
      case 422:
        throw new ValidationError('Invalid data provided', data.error.details);
      case 429:
        throw new RateLimitError('Too many requests, please try again later');
      case 500:
        throw new ServerError('Server error, please try again');
      default:
        throw new ApiError(data.error.message, status);
    }
  }
  
  if (error.request) {
    throw new NetworkError('Network error, please check your connection');
  }
  
  throw new UnknownError('An unexpected error occurred');
};