UTA DevHub

Shared Utilities Guide

Guidelines for organizing and using shared hooks, utilities, and types versus domain-specific code.

Shared Utilities Guide

Overview

This guide provides clear rules and examples for organizing shared hooks, utilities, and types in core/shared/ versus domain-specific code in core/domains/. It ensures clean architecture boundaries and prevents mixing domain logic with truly reusable utilities.

Purpose & Scope

This guide helps developers make correct decisions about where to place code, maintaining the distinction between:

  • Generic, reusable utilities that belong in core/shared/
  • Domain-specific logic that belongs in core/domains/[domain]/

Shared Hooks (core/shared/hooks/)

What Belongs Here

Generic React hooks that:

  • Are truly reusable across multiple features/domains
  • Contain no business logic or domain knowledge
  • Don't depend on domain types or APIs

Examples of Shared Hooks

// core/shared/hooks/useDebounce.ts
import { useState, useEffect } from 'react';
 
export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
 
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
 
  return debouncedValue;
}
// core/shared/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';
 
export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error loading localStorage key "${key}":`, error);
      return initialValue;
    }
  });
 
  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };
 
  return [storedValue, setValue];
}
// core/shared/hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';
 
export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);
 
  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);
 
    const listener = (event: MediaQueryListEvent) => {
      setMatches(event.matches);
    };
 
    media.addEventListener('change', listener);
    return () => media.removeEventListener('change', listener);
  }, [query]);
 
  return matches;
}

What DOESN'T Belong Here

Domain-specific hooks that should stay in core/domains/[domain]/hooks.ts:

// ❌ WRONG: This belongs in core/domains/products/hooks.ts
export function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: productApi.getAll
  });
}
 
// ❌ WRONG: This belongs in core/domains/auth/hooks.ts  
export function useCurrentUser() {
  return useQuery({
    queryKey: ['currentUser'],
    queryFn: authApi.getCurrentUser
  });
}

Shared Utilities (core/shared/utils/)

What Belongs Here

General-purpose JavaScript/TypeScript functions that:

  • Have no domain-specific logic
  • Don't depend on React or domain types
  • Are useful across multiple features/domains

Organization

core/shared/utils/
├── date.ts           # Date formatting and manipulation
├── string.ts         # String utilities
├── number.ts         # Number formatting and calculations
├── validation.ts     # Generic validators
├── array.ts          # Array utilities
└── index.ts         # Barrel exports

Examples of Shared Utilities

// core/shared/utils/date.ts
export function formatDate(date: Date, format: string = 'MM/dd/yyyy'): string {
  // Implementation
  return formattedDate;
}
 
export function addDays(date: Date, days: number): Date {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}
 
export function isDateInPast(date: Date): boolean {
  return date < new Date();
}
// core/shared/utils/string.ts
export function capitalizeFirst(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}
 
export function truncate(str: string, maxLength: number): string {
  return str.length > maxLength ? `${str.slice(0, maxLength)}...` : str;
}
 
export function slugify(str: string): string {
  return str
    .toLowerCase()
    .replace(/[^\w ]+/g, '')
    .replace(/ +/g, '-');
}
// core/shared/utils/number.ts
export function formatCurrency(amount: number, currency: string = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}
 
export function clamp(value: number, min: number, max: number): number {
  return Math.min(Math.max(value, min), max);
}
 
export function roundTo(value: number, decimals: number): number {
  return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
}

What DOESN'T Belong Here

Domain-specific utilities that should stay in domain folders:

// ❌ WRONG: This belongs in core/domains/products/utils.ts (if needed)
export function calculateProductDiscount(price: number, discountPercentage: number) {
  return price * (1 - discountPercentage / 100);
}
 
// ❌ WRONG: This belongs in core/domains/auth/utils.ts
export function parseJWT(token: string) {
  const base64Url = token.split('.')[1];
  // JWT parsing logic
}

Shared Types (core/shared/types/)

What Belongs Here

TypeScript types that:

  • Are used across multiple domains
  • Define generic cross-domain patterns (not component-specific props)
  • Represent common API response patterns
  • Define application-wide constants

Important: Component-specific props (like ButtonProps, CardProps) should be co-located with their components following the Feature Implementation Decision Tree, not placed in shared types.

Organization

core/shared/types/
├── ui.ts            # Cross-domain UI patterns (BaseComponentProps, LoadingState)
├── api.ts           # Generic API types
├── global.ts        # Application-wide types
├── constants.ts     # Shared constants
└── index.ts        # Barrel exports

Examples of Shared Types

// core/shared/types/ui.ts
// Note: Component-specific props like ButtonProps should be co-located 
// with their components per the Feature Implementation Decision Tree.
// Only truly cross-domain interface patterns belong here.
 
export interface BaseComponentProps {
  testID?: string;
  accessible?: boolean;
  accessibilityLabel?: string;
}
 
export interface LoadingState {
  isLoading: boolean;
  error?: Error | null;
}
 
export interface PaginationMeta {
  page: number;
  pageSize: number;
  total: number;
  hasMore: boolean;
}
// core/shared/types/api.ts
export interface ApiResponse<T> {
  data: T;
  error?: string;
  status: number;
}
 
export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}
 
export interface ErrorResponse {
  message: string;
  code: string;
  details?: Record<string, any>;
}
// core/shared/types/global.ts
export interface Theme {
  colors: {
    primary: string;
    secondary: string;
    text: string;
    background: string;
    error: string;
  };
  spacing: {
    xs: number;
    sm: number;
    md: number;
    lg: number;
    xl: number;
  };
}
 
export type AsyncState<T> = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

What DOESN'T Belong Here

Domain-specific types that should stay in domain folders:

// ❌ WRONG: This belongs in core/domains/products/types.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}
 
// ❌ WRONG: This belongs in core/domains/users/types.ts
export interface User {
  id: string;
  email: string;
  profile: UserProfile;
}

Component-specific types that should be co-located with components:

// ❌ WRONG: This belongs in ui/foundation/Button/types.ts
export interface ButtonProps {
  variant?: 'primary' | 'secondary';
  onPress: () => void;
  children: React.ReactNode;
}
 
// ❌ WRONG: This belongs in ui/patterns/Modal/types.ts  
export interface ModalProps {
  visible: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

Migration Checklist

When reviewing existing code to implement this structure:

Step 1: Audit Current Code

  • List all hooks in the project
  • List all utility functions
  • List all type definitions

Step 2: Categorize by Domain

  • Identify which hooks are domain-specific
  • Identify which utilities are domain-specific
  • Identify which types are domain-specific

Step 3: Move Generic Code to Shared

  • Move truly generic hooks to shared/hooks/
  • Move general utilities to shared/utils/
  • Move cross-domain types to shared/types/

Step 4: Keep Domain Code in Domains

  • Ensure domain hooks stay in domains/[domain]/hooks.ts
  • Keep domain utilities in domains (if needed)
  • Keep domain types in domains/[domain]/types.ts

Step 5: Update Imports

  • Fix all import paths
  • Ensure no circular dependencies
  • Update barrel exports

Best Practices

Naming Conventions

All files must follow our File Naming Conventions:

  • Hooks: Always prefix with use (e.g., useDebounce)
  • Utils: Use descriptive verb/noun combinations (e.g., formatDate)
  • Types: Use PascalCase for interfaces/types (e.g., ButtonProps)

File Organization

  • Keep files focused on a single responsibility
  • Group related utilities in the same file
  • Use barrel exports for clean imports

Documentation

/**
 * Debounces a value by the specified delay
 * @param value - The value to debounce
 * @param delay - Delay in milliseconds
 * @returns The debounced value
 */
export function useDebounce<T>(value: T, delay: number): T {
  // Implementation
}

Testing

  • Write unit tests for all shared utilities
  • Test edge cases and error conditions
  • Ensure utilities work in isolation

Common Pitfalls

1. Domain Logic in Shared

Wrong: Putting business logic in shared utilities

// core/shared/utils/product.ts
export function calculateProductTax(product: Product) {
  // This is domain logic!
}

Correct: Keep domain logic in domains

// core/domains/products/utils.ts
export function calculateProductTax(product: Product) {
  // Domain-specific calculation
}

2. Over-Abstraction

Wrong: Creating overly generic utilities

// Too abstract to be useful
export function processData<T>(data: T, processor: (d: T) => T): T {
  return processor(data);
}

Correct: Create specific, useful utilities

// Clear purpose and use case
export function sortByDate<T extends { date: Date }>(items: T[]): T[] {
  return items.sort((a, b) => a.date.getTime() - b.date.getTime());
}

3. Circular Dependencies

Wrong: Shared utilities depending on domains

// core/shared/utils/validation.ts
import { Product } from '@/core/domains/products'; // ❌ domains shouldn't be in shared
 
export function validateProduct(product: Product) {
  // This creates circular dependency
}

Correct: Keep shared utilities independent

// core/shared/utils/validation.ts
export function validateEmail(email: string): boolean {
  // Generic validation, no domain dependencies
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}