docs(31): research selector optimization phase domain
Phase 31: Selector Optimization - Standard stack identified (createSelector, shallowEqual in Redux Toolkit) - Architecture patterns documented (composed selectors, memoization) - Pitfalls catalogued (reference equality, over-subscription, cache thrashing) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
773382d4f7
commit
bb0019c941
|
|
@ -0,0 +1,501 @@
|
||||||
|
# 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)
|
||||||
Loading…
Reference in New Issue