# Phase 31: Selector Optimization - Research

**Researched:** 2026-03-05
**Domain:** Redux Toolkit selector performance optimization (React 16.13.1)
**Confidence:** HIGH

## Summary

Redux selector optimization addresses the performance bottleneck caused by excessive individual `useSelector` calls. The codebase's `useMixerHelper` hook currently uses 18+ individual selectors (lines 72-92), causing unnecessary re-renders when any single mixer field changes. Each selector runs independently, and React 16's default reference equality check (`===`) triggers re-renders even when derived data hasn't meaningfully changed.

The solution involves three complementary techniques: (1) composed selectors using `createSelector` for memoized derived data, (2) `shallowEqual` comparison for object selectors to prevent reference inequality issues, and (3) consolidating related selectors to reduce subscription overhead. Redux Toolkit includes Reselect's `createSelector` by default, making this a zero-dependency optimization.

**Primary recommendation:** Use `createSelector` to compose mixer state into 3-5 memoized selectors instead of 18+ individual ones, apply `shallowEqual` for multi-property object returns, and restructure selectors to group related data that changes together.

## Standard Stack

The established libraries/tools for Redux selector optimization in React 16:

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| @reduxjs/toolkit | 1.6.1 (current) | Includes createSelector | Official Redux toolkit, bundles Reselect |
| react-redux | 7.x-8.x | useSelector hook + shallowEqual | React 16 requires v7/v8 (v9 is React 18 only) |
| reselect | 4.x (bundled) | Memoized selector library | Industry standard, included in RTK |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| use-sync-external-store | 1.6.0 (current) | React 16 shim for external stores | Already in package.json for VU store |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| createSelector | Manual useMemo in hooks | Loses composition, harder to test, couples logic to components |
| shallowEqual | Deep equality library | Much slower, unnecessary for flat objects |
| Redux Toolkit | Standalone Reselect | Requires separate dependency, loses RTK integration |

**Installation:**
```bash
# No installation needed - createSelector is already available via @reduxjs/toolkit
# shallowEqual is already available via react-redux
```

## Architecture Patterns

### Recommended Selector Structure
```
src/store/features/
├── mixersSlice.js
│   ├── Basic selectors (selectMasterMixers, selectPersonalMixers)
│   └── Memoized selectors (selectMixerState, selectDerivedMixerData)
└── selectors/
    └── mixerSelectors.js         # Composed cross-slice selectors (if needed)

src/hooks/
└── useMixerHelper.js             # Uses 3-5 composed selectors, not 18+
```

### Pattern 1: Composed Selector with createSelector
**What:** Combine multiple state slices into a single memoized selector
**When to use:** When multiple pieces of state are needed together and derive new data
**Example:**
```javascript
// Source: https://redux-toolkit.js.org/api/createSelector
import { createSelector } from '@reduxjs/toolkit';

// Input selectors (simple state extraction)
const selectMasterMixers = (state) => state.mixers.masterMixers;
const selectPersonalMixers = (state) => state.mixers.personalMixers;
const selectMixMode = (state) => state.sessionUI.mixMode;

// Composed memoized selector
const selectActiveMixers = createSelector(
  [selectMasterMixers, selectPersonalMixers, selectMixMode],
  (masterMixers, personalMixers, mixMode) => {
    // Only recalculates when inputs change
    return mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;
  }
);

// Usage in component
function useMixerHelper() {
  const activeMixers = useSelector(selectActiveMixers);
  // Single selector subscription instead of 3 separate ones
}
```

### Pattern 2: Object Selector with shallowEqual
**What:** Return multiple fields in an object with shallow equality comparison
**When to use:** When component needs several related fields but derived computation is minimal
**Example:**
```javascript
// Source: https://react-redux.js.org/api/hooks
import { useSelector, shallowEqual } from 'react-redux';

// Without shallowEqual: Creates new object reference every time, always re-renders
const mixerConfig = useSelector(state => ({
  chatMixer: state.mixers.chatMixer,
  broadcastMixer: state.mixers.broadcastMixer,
  recordingMixer: state.mixers.recordingMixer
})); // BAD: Always re-renders

// With shallowEqual: Only re-renders if field values change
const mixerConfig = useSelector(state => ({
  chatMixer: state.mixers.chatMixer,
  broadcastMixer: state.mixers.broadcastMixer,
  recordingMixer: state.mixers.recordingMixer
}), shallowEqual); // GOOD: Re-renders only when fields change
```

### Pattern 3: Grouped Related State
**What:** Consolidate selectors for data that changes together
**When to use:** Multiple fields from the same slice always used together
**Example:**
```javascript
// Source: https://redux.js.org/usage/deriving-data-selectors

// BEFORE: 18+ individual selectors
const chatMixer = useSelector(selectChatMixer);
const broadcastMixer = useSelector(selectBroadcastMixer);
const recordingMixer = useSelector(selectRecordingMixer);
const recordingTrackMixers = useSelector(selectRecordingTrackMixers);
const backingTrackMixers = useSelector(selectBackingTrackMixers);
// ... 13 more individual selectors

// AFTER: Composed selector groups related data
const selectCoreMixerState = createSelector(
  [
    state => state.mixers.chatMixer,
    state => state.mixers.broadcastMixer,
    state => state.mixers.recordingMixer
  ],
  (chatMixer, broadcastMixer, recordingMixer) => ({
    chatMixer,
    broadcastMixer,
    recordingMixer
  })
);

const coreMixers = useSelector(selectCoreMixerState, shallowEqual);
```

### Pattern 4: Parameterized Selectors with useMemo
**What:** Create unique selector instances per component for parameterized lookups
**When to use:** Selector needs arguments that vary per component instance
**Example:**
```javascript
// Source: https://redux.js.org/usage/deriving-data-selectors

// Factory function creates new selector instance
const makeSelectMixerByResourceId = () =>
  createSelector(
    [
      state => state.mixers.mixersByResourceId,
      (state, resourceId) => resourceId
    ],
    (mixersByResourceId, resourceId) => mixersByResourceId[resourceId]
  );

// Component creates unique instance with useMemo
function MixerControl({ resourceId }) {
  const selectMixerByResourceId = useMemo(makeSelectMixerByResourceId, []);
  const mixerPair = useSelector(state =>
    selectMixerByResourceId(state, resourceId)
  );
}
```

### Anti-Patterns to Avoid
- **Don't create new references in useSelector:** `useSelector(state => state.todos.filter(...))` creates new array every render. Use createSelector instead.
- **Don't over-memoize simple lookups:** `createSelector([state => state.value], value => value)` adds overhead for no benefit. Just use `useSelector(state => state.value)`.
- **Don't use createSelector in reducers:** Standard createSelector breaks with Immer drafts. Use `createDraftSafeSelector` if needed in reducers.
- **Don't ignore granularity:** Selecting entire large objects causes re-renders when unused fields change. Select only what's needed.
- **Don't chain useSelectors for derived data:** `const x = useSelector(...); const y = x.map(...)` re-derives on every render. Use createSelector to memoize derivation.

## Don't Hand-Roll

Problems that look simple but have existing solutions:

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Selector memoization | Manual useMemo/useCallback caching | createSelector | Handles input comparison, cache invalidation, composition, and testing automatically |
| Object equality checking | Custom shallow comparison function | shallowEqual from react-redux | Battle-tested, handles edge cases, optimized performance |
| Multi-component selector state | Singleton selector with global cache | Factory functions + useMemo | Prevents cache thrashing when multiple components use same selector with different params |
| Deep equality checking | JSON.stringify comparison | Don't - restructure selectors | Deep equality is slow; indicates selector returns too much data |

**Key insight:** Redux selector optimization is a solved problem with mature tooling. The edge cases (Immer draft handling, cache invalidation, parameterized selectors, composition) are complex and already handled by Redux Toolkit's included Reselect library.

## Common Pitfalls

### Pitfall 1: Always Returning New References
**What goes wrong:** Selector creates new object/array every call, causing re-renders even when data hasn't changed
**Why it happens:** JavaScript's `filter()`, `map()`, object literals `{...}` always create new references
**How to avoid:**
```javascript
// BAD: New array reference every time
const completedTodos = useSelector(state =>
  state.todos.filter(todo => todo.completed)
);

// GOOD: Memoized selector only creates new array when todos change
const selectCompletedTodos = createSelector(
  [state => state.todos],
  todos => todos.filter(todo => todo.completed)
);
const completedTodos = useSelector(selectCompletedTodos);
```
**Warning signs:** Component re-renders after every Redux action, even unrelated ones

### Pitfall 2: Over-Subscription with 18+ Individual Selectors
**What goes wrong:** Each `useSelector` subscribes to Redux store independently, causing 18+ equality checks per action
**Why it happens:** One selector per field feels natural but creates subscription overhead
**How to avoid:** Group related fields into composed selectors
```javascript
// BAD: 18+ subscriptions
const chatMixer = useSelector(selectChatMixer);
const broadcastMixer = useSelector(selectBroadcastMixer);
// ... 16 more

// GOOD: 3-5 composed subscriptions
const coreMixers = useSelector(selectCoreMixerState, shallowEqual);
const trackMixers = useSelector(selectTrackMixerState, shallowEqual);
const derivedData = useSelector(selectDerivedMixerData); // memoized
```
**Warning signs:** Hook runs 18+ times per mixer field change; profiler shows many shallow selector calls

### Pitfall 3: Forgetting shallowEqual with Object Returns
**What goes wrong:** Selector returns object with same field values, but new reference triggers re-render
**Why it happens:** Default useSelector uses `===` reference equality
**How to avoid:**
```javascript
// BAD: Always re-renders (new object reference)
const state = useSelector(state => ({
  a: state.a,
  b: state.b
}));

// GOOD: Only re-renders when a or b changes
const state = useSelector(state => ({
  a: state.a,
  b: state.b
}), shallowEqual);
```
**Warning signs:** Component re-renders even though displayed data hasn't changed

### Pitfall 4: Shared Parameterized Selector Cache Thrashing
**What goes wrong:** Multiple components use same selector with different params, invalidating cache constantly
**Why it happens:** createSelector has single-entry cache by default; different params = different inputs
**How to avoid:** Create unique selector instance per component
```javascript
// BAD: All MixerControl instances share one selector's cache
const selectMixerById = createSelector(
  [state => state.mixers.allMixers, (state, id) => id],
  (allMixers, id) => allMixers[id]
);

function MixerControl({ id }) {
  const mixer = useSelector(state => selectMixerById(state, id));
  // Cache invalidates when other MixerControl with different id renders
}

// GOOD: Each instance has own selector with own cache
function MixerControl({ id }) {
  const selectMixerById = useMemo(() =>
    createSelector(
      [state => state.mixers.allMixers, (state, id) => id],
      (allMixers, id) => allMixers[id]
    ),
    []
  );
  const mixer = useSelector(state => selectMixerById(state, id));
}
```
**Warning signs:** Selector memoization not working; profiler shows recalculations on every render

### Pitfall 5: Using createSelector Inside Reducers
**What goes wrong:** Selector doesn't detect Immer draft changes, returns stale cached data
**Why it happens:** Immer drafts maintain same object reference even when mutated
**How to avoid:** Use `createDraftSafeSelector` in reducers (or don't use selectors in reducers)
**Warning signs:** Selector returns old data inside reducer; updates don't reflect

## Code Examples

Verified patterns from official sources:

### Consolidating 18+ Selectors into Composed Groups
```javascript
// Source: https://redux.js.org/usage/deriving-data-selectors
// Before: useMixerHelper.js lines 72-92 (18+ individual selectors)

import { useSelector, shallowEqual } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';

// Define composed selectors in mixersSlice.js
export const selectCoreMixerState = createSelector(
  [
    state => state.mixers.chatMixer,
    state => state.mixers.broadcastMixer,
    state => state.mixers.recordingMixer
  ],
  (chatMixer, broadcastMixer, recordingMixer) => ({
    chatMixer,
    broadcastMixer,
    recordingMixer
  })
);

export const selectTrackMixerState = createSelector(
  [
    state => state.mixers.recordingTrackMixers,
    state => state.mixers.backingTrackMixers,
    state => state.mixers.jamTrackMixers,
    state => state.mixers.metronomeTrackMixers,
    state => state.mixers.adhocTrackMixers
  ],
  (recordingTrackMixers, backingTrackMixers, jamTrackMixers,
   metronomeTrackMixers, adhocTrackMixers) => ({
    recordingTrackMixers,
    backingTrackMixers,
    jamTrackMixers,
    metronomeTrackMixers,
    adhocTrackMixers
  })
);

export const selectMixerLookupTables = createSelector(
  [
    state => state.mixers.allMixers,
    state => state.mixers.mixersByResourceId,
    state => state.mixers.mixersByTrackId
  ],
  (allMixers, mixersByResourceId, mixersByTrackId) => ({
    allMixers,
    mixersByResourceId,
    mixersByTrackId
  })
);

// Use in useMixerHelper hook
function useMixerHelper() {
  // 3 selectors instead of 18+
  const coreMixers = useSelector(selectCoreMixerState, shallowEqual);
  const trackMixers = useSelector(selectTrackMixerState, shallowEqual);
  const lookupTables = useSelector(selectMixerLookupTables, shallowEqual);

  // Destructure for backward compatibility
  const { chatMixer, broadcastMixer, recordingMixer } = coreMixers;
  const { recordingTrackMixers, backingTrackMixers, jamTrackMixers,
          metronomeTrackMixers, adhocTrackMixers } = trackMixers;
  const { allMixers, mixersByResourceId, mixersByTrackId } = lookupTables;

  // Rest of hook logic unchanged
}
```

### Memoizing Derived Data Calculations
```javascript
// Source: https://redux-toolkit.js.org/api/createSelector
// For expensive transformations or filtering

export const selectMasterMixersForMode = createSelector(
  [
    state => state.mixers.masterMixers,
    state => state.mixers.personalMixers,
    state => state.sessionUI.mixMode
  ],
  (masterMixers, personalMixers, mixMode) => {
    // This only recalculates when inputs change
    return mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;
  }
);

// More complex derived data
export const selectSimulatedCategoryMixers = createSelector(
  [
    selectMasterMixers,
    selectPersonalMixers,
    selectNoAudioUsers,
    selectClientsWithAudioOverride
  ],
  (masterMixers, personalMixers, noAudioUsers, clientsWithAudioOverride) => {
    // Complex calculation only runs when dependencies change
    const musicMixers = masterMixers.filter(m =>
      m.group_id === ChannelGroupIds.AudioInputMusicGroup
    );
    const chatMixers = masterMixers.filter(m =>
      m.group_id === ChannelGroupIds.ChatMicGroup
    );

    return {
      music: { MASTER: musicMixers[0], PERSONAL: personalMixers[0] },
      chat: { MASTER: chatMixers[0], PERSONAL: personalMixers[0] }
    };
  }
);
```

### Testing Composed Selectors
```javascript
// Source: https://redux.js.org/usage/deriving-data-selectors
// Selectors are pure functions - easy to unit test

import { selectCoreMixerState } from './mixersSlice';

describe('selectCoreMixerState', () => {
  test('returns core mixer state object', () => {
    const state = {
      mixers: {
        chatMixer: { id: 1 },
        broadcastMixer: { id: 2 },
        recordingMixer: { id: 3 }
      }
    };

    const result = selectCoreMixerState(state);

    expect(result).toEqual({
      chatMixer: { id: 1 },
      broadcastMixer: { id: 2 },
      recordingMixer: { id: 3 }
    });
  });

  test('memoizes result when inputs unchanged', () => {
    const state = {
      mixers: {
        chatMixer: { id: 1 },
        broadcastMixer: { id: 2 },
        recordingMixer: { id: 3 }
      }
    };

    const result1 = selectCoreMixerState(state);
    const result2 = selectCoreMixerState(state);

    // Same reference = memoization working
    expect(result1).toBe(result2);
  });
});
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Multiple useSelector per field | Composed selectors with createSelector | Reselect 2.0+ (2016) | Reduced re-renders, better memoization |
| Manual memoization with useMemo | createSelector in slice files | Redux Toolkit 1.0 (2019) | Easier testing, composition, reusability |
| mapStateToProps (connect HOC) | useSelector hook | React-Redux 7.1 (2019) | Better performance with granular subscriptions |
| Custom equality functions | shallowEqual utility | React-Redux built-in | Standardized, optimized implementation |
| Separate reselect dependency | Bundled in Redux Toolkit | Redux Toolkit 1.0 (2019) | Zero additional dependencies |

**Deprecated/outdated:**
- **mapStateToProps with connect()**: Still works but hooks (useSelector) are preferred for functional components
- **Redux Toolkit's configureStore includes reselect**: Don't install reselect separately unless you need advanced features not in RTK
- **String keypaths in createSelector**: Removed in Reselect 4.0 due to TypeScript typing issues

## Open Questions

Things that couldn't be fully resolved:

1. **Cache Size Configuration**
   - What we know: createSelector uses single-entry cache by default; Reselect 4.1+ supports custom cache sizes
   - What's unclear: Whether Redux Toolkit 1.6.1 (project's version) includes newer Reselect with configurable cache
   - Recommendation: Use factory pattern + useMemo for parameterized selectors (works with any Reselect version)

2. **Optimal Granularity Balance**
   - What we know: 18+ selectors is excessive; 1 giant selector is also suboptimal
   - What's unclear: Exact threshold where consolidation stops being beneficial (depends on update patterns)
   - Recommendation: Group selectors by logical domains (core mixers, track mixers, lookup tables) and measure with React DevTools Profiler

3. **React 18 Migration Impact**
   - What we know: React-Redux v9 requires React 18; current project uses React 16.13.1 with React-Redux 7.x/8.x
   - What's unclear: Whether future React 18 upgrade changes selector optimization strategies
   - Recommendation: Current patterns (createSelector, shallowEqual) remain valid in React 18; no strategy change needed

## Sources

### Primary (HIGH confidence)
- [Redux Toolkit createSelector API](https://redux-toolkit.js.org/api/createSelector) - Official documentation on memoized selectors
- [Redux: Deriving Data with Selectors](https://redux.js.org/usage/deriving-data-selectors) - Official guide on selector patterns and best practices
- [React-Redux Hooks API](https://react-redux.js.org/api/hooks) - useSelector and shallowEqual documentation
- [Redux Performance Optimization](https://redux.js.org/tutorials/essentials/part-6-performance-normalization) - Official performance guidance

### Secondary (MEDIUM confidence)
- [Redux Style Guide](https://redux.js.org/style-guide/) - Official recommendations for Redux patterns
- [GitHub: Reselect Library](https://github.com/reduxjs/reselect) - Source code and advanced patterns
- [Redux Toolkit Migration Guide](https://redux-toolkit.js.org/usage/migrating-rtk-2) - Compatibility information
- [Medium: Unleashing Redux Performance with useSelector](https://medium.com/@ProPhycientSawan/unleashing-redux-performance-with-useselector-e895c4d7b91b) - Performance analysis

### Tertiary (LOW confidence)
- Various blog posts on selector optimization - General patterns confirmed by official docs

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - Redux Toolkit 1.6.1 includes everything needed; official docs confirm patterns
- Architecture: HIGH - Official Redux docs provide clear guidance on selector composition and structure
- Pitfalls: HIGH - Documented in official Redux tutorials with examples and explanations

**Research date:** 2026-03-05
**Valid until:** 60 days (stable ecosystem, Redux Toolkit 2.x released but project on 1.6.1)
