# 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)