Business Logic Separation Guide
This guide demonstrates patterns for separating business logic from UI components and state management in the Context-Action framework.
Related Documentation:
- Store Conventions - Store types and usage patterns
- Conventions - Overall coding conventions
- Action Pipeline Guide - Action handler patterns
Business Logic Separation
Overview
Separating business logic from UI components and state management is crucial for maintainability, testability, and scalability. The Context-Action framework supports this through modular business logic patterns with async process state management.
Core Principles
- Pure Business Logic: Business logic should be independent of React and store implementations
- State Machines: Use explicit state transitions for complex async processes
- Progress Decoupling: Separate progress updates from state changes using
notifyPath - Modular Design: Business logic modules should be testable without UI
Pattern 1: Business Logic Module
Create pure business logic classes or functions independent of React/stores:
// ✅ Pure business logic module
class FileUploadService {
/**
* Validate file (pure function)
*/
validateFile(file: File): { valid: boolean; error?: string } {
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
return { valid: false, error: 'File too large' };
}
return { valid: true };
}
/**
* Upload with progress callbacks
*/
async uploadFile(
file: File,
onProgress: (progress: number) => void
): Promise<{ fileId: string }> {
// Upload implementation with progress tracking
for (let i = 0; i <= 100; i += 10) {
await delay(100);
onProgress(i);
}
return { fileId: `file_${Date.now()}` };
}
/**
* Complete workflow orchestration
*/
async completeUpload(
file: File,
callbacks: {
onStateChange: (state: ProcessState) => void;
onProgress: (progress: number) => void;
}
): Promise<UploadResult> {
try {
callbacks.onStateChange('validating');
const validation = this.validateFile(file);
if (!validation.valid) {
callbacks.onStateChange('error');
return { success: false, error: validation.error };
}
callbacks.onStateChange('uploading');
const { fileId } = await this.uploadFile(file, callbacks.onProgress);
callbacks.onStateChange('complete');
return { success: true, fileId };
} catch (error) {
callbacks.onStateChange('error');
return { success: false, error: error.message };
}
}
}Benefits:
- ✅ Testable without React or stores
- ✅ Reusable across different UI frameworks
- ✅ Clear separation of concerns
- ✅ Easy to mock and test
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Business Logic Separation')- Pure business logic without dependenciesFileUploadServiceclass implementation
Pattern 2: Async Process State Machine
Use explicit state types and transitions for complex async workflows:
// State machine type
type ProcessState =
| 'idle'
| 'validating'
| 'uploading'
| 'processing'
| 'complete'
| 'error';
// Store with state machine
const processStore = createTimeTravelStore('process', {
state: 'idle' as ProcessState,
progress: { percentage: 0, bytesUploaded: 0, totalBytes: 0 },
error: null as string | null,
result: null as UploadResult | null
}, { mutable: true });
// State transitions with notifyPath
async function executeUpload(file: File) {
const uploadService = new FileUploadService();
await uploadService.completeUpload(file, {
// State transition (notifyPath only)
onStateChange: (state) => {
const current = processStore.getValue();
current.state = state;
processStore.notifyPath(['state']);
},
// Progress update (notifyPath only)
onProgress: (percentage) => {
const current = processStore.getValue();
current.progress.percentage = percentage;
processStore.notifyPath(['progress', 'percentage']);
}
});
}State Machine Benefits:
- ✅ Explicit state transitions
- ✅ Easy to visualize workflow
- ✅ Prevents invalid states
- ✅ Facilitates debugging
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Async Process State Machine')- State machine pattern with notifyPathit('proves state machine pattern with notifyPath for state-only updates')- Full workflow test
Pattern 3: Progress-Only Updates
Decouple progress updates from state changes for maximum performance:
const uploadStore = createTimeTravelStore('upload', {
state: 'uploading' as ProcessState,
progress: { current: 0, total: 100 },
file: null as File | null
}, { mutable: true });
// Component subscribes to specific paths
function UploadProgress() {
// Only re-renders when progress changes
const progress = useStorePath(uploadStore, ['progress']);
return <ProgressBar value={progress.current} max={progress.total} />;
}
function UploadState() {
// Only re-renders when state changes
const state = useStorePath(uploadStore, ['state']);
return <StatusText state={state} />;
}
// Update progress WITHOUT triggering state re-render
function updateProgress(current: number) {
const store = uploadStore.getValue();
store.progress.current = current;
// Only UploadProgress component re-renders
uploadStore.notifyPath(['progress', 'current']);
}
// Update state WITHOUT triggering progress re-render
function updateState(newState: ProcessState) {
const store = uploadStore.getValue();
store.state = newState;
// Only UploadState component re-renders
uploadStore.notifyPath(['state']);
}Progress Decoupling Benefits:
- ✅ 100% re-render efficiency (no wasted renders)
- ✅ High-frequency updates without performance cost
- ✅ Independent component subscriptions
- ✅ Optimal for progress bars, loading indicators
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Async Process State Machine')- Progress-only updates testit('proves progress-only updates do not trigger state re-renders')- 10 progress updates, 0 state renders
Pattern 4: Modular Integration
Integrate business logic, state management, and UI with clear boundaries:
// 1. Business Logic Layer (no dependencies)
const uploadService = new FileUploadService();
// 2. State Management Layer
const uploadFlowStore = createTimeTravelStore('uploadFlow', {
files: [] as Array<{ id: string; state: ProcessState; progress: number }>,
activeUpload: null as { fileId: string; state: ProcessState } | null
}, { mutable: true });
// 3. Orchestration Layer (connects business logic + state)
async function processUploadQueue(files: File[]) {
const state = uploadFlowStore.getValue();
for (const file of files) {
const fileId = `file_${Date.now()}`;
// Add to queue
state.files.push({ id: fileId, state: 'idle', progress: 0 });
uploadFlowStore.notifyPath(['files']);
// Set active
state.activeUpload = { fileId, state: 'uploading' };
uploadFlowStore.notifyPath(['activeUpload']);
// Execute business logic
await uploadService.completeUpload(file, {
onStateChange: (uploadState) => {
const current = uploadFlowStore.getValue();
if (current.activeUpload) {
current.activeUpload.state = uploadState;
uploadFlowStore.notifyPath(['activeUpload', 'state']);
}
},
onProgress: (percentage) => {
const current = uploadFlowStore.getValue();
const fileIndex = current.files.findIndex(f => f.id === fileId);
if (fileIndex >= 0) {
current.files[fileIndex].progress = percentage;
uploadFlowStore.notifyPath(['files', fileIndex, 'progress']);
}
}
});
// Complete
state.activeUpload = null;
uploadFlowStore.notifyPath(['activeUpload']);
}
}
// 4. UI Layer (pure presentation)
function UploadQueue() {
const files = useStorePath(uploadFlowStore, ['files']);
return (
<div>
{files.map(file => (
<FileItem key={file.id} file={file} />
))}
</div>
);
}
function FileItem({ file }) {
return (
<div>
<span>{file.state}</span>
<ProgressBar value={file.progress} />
</div>
);
}Integration Benefits:
- ✅ Business logic testable in isolation
- ✅ State management decoupled from business logic
- ✅ UI components are pure presentation
- ✅ Clear separation of concerns
- ✅ Each layer independently testable
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Modular Business Logic Integration')- Complete integration testit('proves integration of business logic, state management, and selective rendering')- Full layer separation proof
Pattern 5: Error Handling with State Machine
Handle errors through explicit state transitions:
const errorStore = createTimeTravelStore('error', {
state: 'idle' as ProcessState,
error: null as { code: string; message: string } | null,
retryCount: 0,
maxRetries: 3
}, { mutable: true });
async function executeWithErrorHandling(operation: () => Promise<void>) {
const state = errorStore.getValue();
try {
state.state = 'processing';
state.error = null;
errorStore.notifyPaths([['state'], ['error']]);
await operation();
state.state = 'complete';
state.retryCount = 0;
errorStore.notifyPaths([['state'], ['retryCount']]);
} catch (error) {
state.state = 'error';
state.error = {
code: error.code || 'UNKNOWN_ERROR',
message: error.message
};
state.retryCount++;
errorStore.notifyPaths([
['state'],
['error'],
['retryCount']
]);
// Auto-retry logic
if (state.retryCount < state.maxRetries) {
await delay(1000 * state.retryCount); // Exponential backoff
await executeWithErrorHandling(operation);
}
}
}Error Handling Benefits:
- ✅ Explicit error states
- ✅ Retry logic with backoff
- ✅ Error details tracked
- ✅ Clear failure recovery paths
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Error Handling with State Machine')- Error state management testit('proves error state management with notifyPath')- Validation error handling
Pattern 6: Multi-file Queue Management
Complex workflow with multiple concurrent operations:
const queueStore = createTimeTravelStore('queue', {
queue: [] as Array<{
id: string;
state: ProcessState;
progress: number;
error: string | null;
}>,
processing: false,
currentIndex: -1,
completedCount: 0,
failedCount: 0
}, { mutable: true });
async function processQueue(files: File[]) {
const state = queueStore.getValue();
// Initialize queue
state.queue = files.map((file, i) => ({
id: `file_${i}`,
state: 'idle' as ProcessState,
progress: 0,
error: null
}));
state.processing = true;
queueStore.notifyPaths([['queue'], ['processing']]);
// Process each file
for (let i = 0; i < files.length; i++) {
state.currentIndex = i;
state.queue[i].state = 'uploading';
queueStore.notifyPaths([
['currentIndex'],
['queue', i, 'state']
]);
try {
await uploadService.uploadFile(files[i], (progress) => {
// Progress-only update
state.queue[i].progress = progress;
queueStore.notifyPath(['queue', i, 'progress']);
});
state.queue[i].state = 'complete';
state.completedCount++;
queueStore.notifyPaths([
['queue', i, 'state'],
['completedCount']
]);
} catch (error) {
state.queue[i].state = 'error';
state.queue[i].error = error.message;
state.failedCount++;
queueStore.notifyPaths([
['queue', i, 'state'],
['queue', i, 'error'],
['failedCount']
]);
}
}
state.processing = false;
state.currentIndex = -1;
queueStore.notifyPaths([['processing'], ['currentIndex']]);
}Queue Management Benefits:
- ✅ Per-item state tracking
- ✅ Selective path updates
- ✅ Batch notifications with
notifyPaths - ✅ No unnecessary re-renders
- ✅ Global queue status tracking
Test Reference: See packages/react/__tests__/stores/notifyPath-async-process.test.tsx
describe('Complex Workflow: Multi-file Upload Queue')- Queue management testit('proves complex async workflow with queue management')- 3 files, 5 progress updates each
Testing Business Logic
// Test business logic WITHOUT React or stores
describe('FileUploadService', () => {
it('validates file size', () => {
const service = new FileUploadService();
const largeFile = new File(['x'.repeat(20 * 1024 * 1024)], 'huge.pdf');
const result = service.validateFile(largeFile);
expect(result.valid).toBe(false);
expect(result.error).toContain('10MB');
});
it('uploads with progress tracking', async () => {
const service = new FileUploadService();
const file = new File(['content'], 'test.pdf');
const progressUpdates: number[] = [];
await service.uploadFile(file, (progress) => {
progressUpdates.push(progress);
});
expect(progressUpdates.length).toBeGreaterThan(0);
expect(progressUpdates[progressUpdates.length - 1]).toBe(100);
});
});Performance Characteristics
Traditional setValue Approach:
// ❌ Multiple re-renders for single operation
store.setValue({ ...state, loading: true }); // Re-render 1
store.setValue({ ...state, progress: 50 }); // Re-render 2
store.setValue({ ...state, loading: false }); // Re-render 3
// Total: 3 re-rendersOptimized notifyPath Approach:
// ✅ Selective updates, minimal re-renders
state.loading = true;
store.notifyPath(['loading']); // Notification only
state.progress = 50;
store.notifyPath(['progress']); // Progress update only
state.loading = false;
store.setValue(state); // Final re-render
// Total: 1 re-render + 2 selective notificationsBest Practices Summary
- Separate Business Logic: Keep business logic independent of React/stores
- Use State Machines: Explicit states for complex async workflows
- Decouple Progress: Use
notifyPathfor progress without state changes - Path-based Subscriptions:
useStorePathfor selective re-rendering - Batch Updates: Use
notifyPathsfor multiple path updates - Test in Isolation: Unit test business logic without UI dependencies
- Clear Boundaries: Business → State → UI layer separation
- Error States: Explicit error handling through state machine
Related Documentation
Performance & Testing:
- Performance Proof - Mathematical performance proofs (50% re-render reduction, RAF batching, etc.)
- Async Process Tests - Comprehensive test suite with 6 test categories:
- Business Logic Separation - Pure business logic without dependencies
- Async Process State Machine - State transitions with notifyPath
- Progress-Only Updates - Selective re-rendering proof
- Modular Business Logic Integration - Full layer separation
- Error Handling - Validation and error states
- Multi-file Queue Management - Complex concurrent workflows
- Performance Tests - Re-render reduction, RAF batching, selective rendering benchmarks
Architecture & Patterns:
- Event Loop Control - Integration patterns with Action Handlers and RefContext
- Main Conventions - Overall framework conventions
- Hooks Reference - Complete hooks documentation