← Back to documentation

compact-compare-card-state

Compact Compare-Card State System Design

Problem Statement

The current ProductComparison component uses two separate useState hooks and multiple derived computations:

const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [selectCount, setSelectCount] = useState<2 | 3 | 4>(2);

This approach has inefficiencies:

  • Array operations (filter, includes, push) are O(n)
  • No atomic state transitions for multi-field updates
  • State machine transitions are implicit, not explicit

Compact State Design

State Representation

Replace multiple state variables with a single unified state object using a bitfield for selections:

interface CompareCardState {
  // Bitfield: each bit represents selection status
  // For max 4 products: bits 0-3 = selected, bits 4-7 = selection order
  selection: number;  // compact bitmask representation
  
  // Single state machine field instead of derived state
  phase: 'idle' | 'selecting' | 'comparing' | 'error';
  
  // Configuration
  selectCount: 2 | 3 | 4;
  
  // Error state
  error?: { code: string; message: string };
}

Bitfield Encoding

selection: 0000_0000
           ||||____ bit 0-3: selection bitmask (1=selected)
           ||||
           ||||_____ bit 4: product 0 selected
           |||______ bit 5: product 1 selected
           ||_______ bit 6: product 2 selected
           |________ bit 7: product 3 selected
           
Example: Product 0 and 2 selected = 0b0101_0000 = 80

Selection order encoded in remaining bits allows restoration of selection order for display.

Derived State Accessors

const getSelectedIds = (state: CompareCardState): number[] => {
  const mask = state.selection & 0x0F;
  const ids: number[] = [];
  for (let i = 0; i < 4; i++) {
    if (mask & (1 << i)) ids.push(i);
  }
  return ids;
};

const getSelectionOrder = (state: CompareCardState): number[] => {
  const orderMask = (state.selection >> 4) & 0x0F;
  const order: number[] = [];
  for (let i = 0; i < 4; i++) {
    if (orderMask & (1 << i)) order.push(i);
  }
  return order;
};

State Transitions

idle ─────────────────────────────────────────────────────────
  │                                                          │
  ▼                                                          │
selecting ──────────────────────────────────────────────────
  │                                                          │
  ├── [select product] ───► selecting (updated bitfield)     │
  ├── [remove product] ───► selecting (updated bitfield)      │
  ├── [selectCount change] ► selecting (trim selections)     │
  │                                                          │
  ▼                                                          │
comparing ──────────────────────────────────────────────────
  │                                                          │
  ├── [back action] ────► selecting                         │
  ├── [compare success] ► comparing                         │
  └── [error] ─────────► error                              │
  │                                                          │
  ▼                                                          │
error ───────────────────────────────────────────────────────
  │                                                          │
  └── [retry] ─────────► selecting                          │

Compact Actions

type CompareCardAction =
  | { type: 'SELECT'; productId: number }
  | { type: 'DESELECT'; productId: number }
  | { type: 'SET_COUNT'; count: 2 | 3 | 4 }
  | { type: 'TOGGLE'; productId: number }
  | { type: 'RESET' }
  | { type: 'COMPARE' }
  | { type: 'BACK' }
  | { type: 'ERROR'; code: string; message: string }
  | { type: 'CLEAR_ERROR' };

Reducer Implementation

function compareCardReducer(state: CompareCardState, action: CompareCardAction): CompareCardState {
  switch (action.type) {
    case 'SELECT': {
      if (state.phase !== 'selecting') return state;
      const currentCount = popcount(state.selection & 0x0F);
      if (currentCount >= state.selectCount) return state; // full
      
      const newSelection = state.selection | (1 << action.productId) | ((1 << currentCount) << 4);
      return { ...state, selection: newSelection };
    }
    
    case 'DESELECT': {
      if (state.phase !== 'selecting') return state;
      const newSelection = state.selection & ~(1 << action.productId);
      return { ...state, selection: newSelection };
    }
    
    case 'SET_COUNT': {
      const newSelection = trimToCount(state.selection, action.count);
      return { ...state, selectCount: action.count, selection: newSelection };
    }
    
    case 'TOGGLE': {
      if (state.selection & (1 << action.productId)) {
        return compareCardReducer(state, { type: 'DESELECT', productId: action.productId });
      }
      return compareCardReducer(state, { type: 'SELECT', productId: action.productId });
    }
    
    case 'RESET': {
      return { ...initialState };
    }
    
    case 'COMPARE': {
      if (state.phase !== 'selecting') return state;
      return { ...state, phase: 'comparing' };
    }
    
    case 'BACK': {
      if (state.phase !== 'comparing') return state;
      return { ...state, phase: 'selecting' };
    }
    
    case 'ERROR': {
      return { ...state, phase: 'error', error: { code: action.code, message: action.message } };
    }
    
    case 'CLEAR_ERROR': {
      return { ...state, phase: 'selecting', error: undefined };
    }
  }
}

Memory Efficiency

RepresentationSize (bytes)Notes
number[] + `234`
Bitfield state16 bytesSingle object, no array allocation
Savings~32 bytes67% reduction per instance

Implementation Plan

  1. Create hooks/useCompareCardState.ts with reducer and context
  2. Update ProductComparison.tsx to use new hook
  3. Update types/compare.ts with compact types
  4. Add unit tests for reducer

Backward Compatibility

The new state system maintains API compatibility:

  • selectedIds derived via getSelectedIds(state)
  • selectCount accessed via state.selectCount
  • selectedProducts computed on-demand from derived state

Migration Path

// Before
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [selectCount, setSelectCount] = useState<2 | 3 | 4>(2);

// After
const [state, dispatch] = useCompareCardState({ selectCount: 2 });
const selectedIds = getSelectedIds(state);

This enables future optimizations like:

  • useReducer with useTransition for non-blocking updates
  • Serialization to URL params for shareable comparison state
  • Persistence to sessionStorage for tab recovery