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
| Representation | Size (bytes) | Notes |
|---|---|---|
number[] + `2 | 3 | 4` |
| Bitfield state | 16 bytes | Single object, no array allocation |
| Savings | ~32 bytes | 67% reduction per instance |
Implementation Plan
- Create
hooks/useCompareCardState.tswith reducer and context - Update
ProductComparison.tsxto use new hook - Update
types/compare.tswith compact types - Add unit tests for reducer
Backward Compatibility
The new state system maintains API compatibility:
selectedIdsderived viagetSelectedIds(state)selectCountaccessed viastate.selectCountselectedProductscomputed 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:
useReducerwithuseTransitionfor non-blocking updates- Serialization to URL params for shareable comparison state
- Persistence to
sessionStoragefor tab recovery