Skip to main content

Code Organization

Core Principles

1. Separation of Actions from Calculations

  • Calculations (Pure Functions)

    • Return the same output for the same input
    • Have no side effects
    • Can be safely unit tested
    • Examples: data transformations, validations, computations
  • Actions (Functions with Side Effects)

    • Interact with the outside world
    • May have different results for the same input
    • Need to be carefully managed
    • Examples: API calls, database operations, state updates

2. First-Class Functions

  • Actions and calculations are defined as separate functions that can be passed around
  • Functions are composed together in hooks for maximum reusability
  • Example:
    // Calculation (Pure Function)
    const filterActiveUsers = (users) => users.filter(user => user.isActive);

    // Action
    const useUserManagement = () => {
    const [users, setUsers] = useState([]);

    // Composing functions
    const getActiveUsers = () => filterActiveUsers(users);

    return { users, getActiveUsers };
    };

3. Pure Functions

  • All calculations must be pure functions
  • Same input always produces same output
  • No side effects or external state dependencies
  • Example:
    // Pure Function
    const calculateTotal = (items) => {
    return items.reduce((sum, item) => sum + item.price, 0);
    };

    // Impure Function (depends on external state)
    const calculateTotalWithTax = (items) => {
    return items.reduce((sum, item) => sum + item.price, 0) * globalTaxRate;
    };

4. Immutability

  • Never modify state directly
  • Create new state objects using spread operator
  • Use array methods that return new arrays (map, filter, reduce)
  • Example:
    // Immutable update
    const addItem = (items, newItem) => [...items, newItem];

    // Mutable update
    const addItem = (items, newItem) => {
    items.push(newItem); // Mutates original array
    return items;
    };

5. Unidirectional Data Flow

  • Clear flow: State → Calculations → UI
  • State changes only through defined actions
  • Example:
    const useTaskManager = () => {
    // State
    const [tasks, setTasks] = useState([]);

    // Calculations
    const incompleteTasks = () => tasks.filter(task => !task.completed);
    const completedTasks = () => tasks.filter(task => task.completed);

    // Actions
    const addTask = (task) => setTasks([...tasks, task]);
    const toggleTask = (taskId) => {
    setTasks(tasks.map(task =>
    task.id === taskId
    ? { ...task, completed: !task.completed }
    : task
    ));
    };

    return {
    // Expose calculated values
    incomplete: incompleteTasks(),
    completed: completedTasks(),
    // Expose actions
    addTask,
    toggleTask
    };
    };

6. Abstraction

  • Complex logic hidden behind hooks
  • Components only depend on the hook's interface
  • Example:
    // Complex logic hidden in hook
    const useDataProcessor = () => {
    // ... complex data processing logic ...
    return {
    processedData,
    isLoading,
    error,
    process
    };
    };

    // Simple component interface
    const DataView = () => {
    const { processedData, isLoading, error, process } = useDataProcessor();

    if (isLoading) return <Loading />;
    if (error) return <Error message={error} />;

    return <DisplayData data={processedData} onProcess={process} />;
    };

React Hooks Organization

We follow a layered approach to organize our React hooks:

1. Business Logic Hooks (Custom Hooks)

  • Separate business logic from UI components
  • Handle complex state management
  • Manage API calls and data transformations
  • Example:
    const useUserData = (userId: string) => {
    // Business logic for fetching and managing user data
    const [userData, setUserData] = useState(null);

    // Separate calculations
    const transformUserData = (rawData) => {
    // Pure function to transform data
    return /* transformed data */;
    };

    // Actions are clearly identified
    const fetchUser = async () => {
    const data = await api.getUser(userId);
    setUserData(transformUserData(data));
    };

    return { userData, fetchUser };
    };

2. UI Component Hooks

  • Focus on presentation logic
  • Handle UI state and interactions
  • Example:
    const UserProfile = ({ userId }) => {
    // Business logic is separated into custom hook
    const { userData, fetchUser } = useUserData(userId);

    // UI-specific state
    const [isEditing, setIsEditing] = useState(false);

    return (/* UI JSX */);
    };

Testing Strategy

Testing Calculations

  • Write comprehensive unit tests
  • Test all edge cases
  • No need for mocking
describe('filterActiveUsers', () => {
it('should return only active users', () => {
const users = [
{ id: 1, isActive: true },
{ id: 2, isActive: false }
];
expect(filterActiveUsers(users)).toEqual([{ id: 1, isActive: true }]);
});
});

Testing Actions

  • Use dependency injection for testability
  • Mock external dependencies
  • Test side effects
describe('useTaskManager', () => {
it('should properly manage task state', () => {
const { result } = renderHook(() => useTaskManager());

act(() => {
result.current.addTask({ id: 1, text: 'Test', completed: false });
});

expect(result.current.incomplete).toHaveLength(1);
expect(result.current.completed).toHaveLength(0);
});
});

Benefits

  • Improved testability through pure functions
  • Clear separation of concerns
  • Predictable state management
  • Highly reusable code
  • Self-documenting architecture
  • Reduced bugs through immutability