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