21 KiB
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:
# 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:
// 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:
// 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:
// 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:
// 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 useuseSelector(state => state.value). - Don't use createSelector in reducers: Standard createSelector breaks with Immer drafts. Use
createDraftSafeSelectorif 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:
// 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
// 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:
// 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
// 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
// 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
// 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
// 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:
-
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)
-
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
-
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 - Official documentation on memoized selectors
- Redux: Deriving Data with Selectors - Official guide on selector patterns and best practices
- React-Redux Hooks API - useSelector and shallowEqual documentation
- Redux Performance Optimization - Official performance guidance
Secondary (MEDIUM confidence)
- Redux Style Guide - Official recommendations for Redux patterns
- GitHub: Reselect Library - Source code and advanced patterns
- Redux Toolkit Migration Guide - Compatibility information
- Medium: Unleashing Redux Performance with useSelector - 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)