@codyswann/lisa 1.55.2 → 1.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,251 @@
1
+ ---
2
+ name: reduce-complexity
3
+ description: This skill provides strategies and patterns for reducing cognitive complexity in React components. It should be used when ESLint reports sonarjs/cognitive-complexity violations, when refactoring complex View components, or when planning how to break down large components. The skill enforces this project's Container/View pattern requirements when extracting components.
4
+ ---
5
+
6
+ # Reduce Complexity
7
+
8
+ This skill provides systematic approaches for reducing cognitive complexity in React components while adhering to this project's Container/View pattern requirements.
9
+
10
+ ## When to Use This Skill
11
+
12
+ - ESLint reports `sonarjs/cognitive-complexity` violations (threshold: 28)
13
+ - A View component exceeds 200 lines
14
+ - A component has deeply nested conditionals or repeated patterns
15
+ - Planning refactoring of a complex component
16
+ - Deciding between extracting helper functions vs full components
17
+
18
+ ## Complexity Sources in React Components
19
+
20
+ Cognitive complexity increases with:
21
+
22
+ | Source | Complexity Impact | Common in View Components |
23
+ | ---------------------- | ----------------- | ------------------------- |
24
+ | Nested conditionals | +1 per nesting | Yes |
25
+ | Ternary expressions | +1 each | Yes |
26
+ | Logical operators (&&) | +1 each | Yes |
27
+ | Loops (map, filter) | +1 each | Yes |
28
+ | Switch/case statements | +1 per case | Rare |
29
+ | Catch blocks | +1 each | No (Container only) |
30
+ | Nested functions | +1 per nesting | Yes |
31
+
32
+ ## Decision Framework: Helper Function vs Full Component
33
+
34
+ Before extracting code, determine the appropriate strategy:
35
+
36
+ ### Extract as Helper Function When:
37
+
38
+ - The JSX renders a static or simple section with no logic of its own
39
+ - The section does not need its own state, hooks, or callbacks
40
+ - The pattern appears only in this file
41
+ - The complexity comes from rendering, not behavior
42
+
43
+ ```tsx
44
+ // Helper function - no logic, just rendering
45
+ function renderSectionHeader(props: {
46
+ readonly title: string;
47
+ readonly count: number;
48
+ }) {
49
+ return (
50
+ <HStack className="justify-between">
51
+ <Text className="font-bold">{props.title}</Text>
52
+ <Text className="text-sm">({props.count})</Text>
53
+ </HStack>
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Extract as Full Component (Container/View) When:
59
+
60
+ - The section has reusable logic or could be used elsewhere
61
+ - The section would benefit from its own state management
62
+ - The pattern repeats across multiple files
63
+ - The section has 3+ props that could be simplified with a Container
64
+ - Extracting would create a meaningful, named abstraction
65
+
66
+ ```
67
+ FilterChipList/
68
+ ├── FilterChipListContainer.tsx # Handles selection logic
69
+ ├── FilterChipListView.tsx # Renders chip list
70
+ └── index.tsx # Exports Container
71
+ ```
72
+
73
+ ## Refactoring Process
74
+
75
+ ### Step 1: Analyze Complexity Sources
76
+
77
+ Run ESLint to identify the violation:
78
+
79
+ ```bash
80
+ bun run lint 2>&1 | grep "cognitive-complexity"
81
+ ```
82
+
83
+ > **Note:** Replace `bun` with your project's package manager (`npm`, `yarn`, `pnpm`) as needed.
84
+
85
+ Read the file and identify:
86
+
87
+ 1. Which function has the violation (line number from ESLint)
88
+ 2. What patterns repeat (copy-pasted JSX with slight variations)
89
+ 3. What conditionals nest deeply (ternaries inside ternaries)
90
+ 4. What could be pre-computed in Container
91
+
92
+ ### Step 2: Choose Extraction Strategy
93
+
94
+ Use the decision framework above. For View components:
95
+
96
+ | Situation | Strategy |
97
+ | -------------------------- | --------------------------------- |
98
+ | Repeated JSX, no logic | Helper function |
99
+ | Repeated JSX, needs props | Helper function with props object |
100
+ | Repeated pattern, 3+ files | Full Container/View component |
101
+ | Complex section, own state | Full Container/View component |
102
+ | Deeply nested ternaries | Pre-compute flags in Container |
103
+
104
+ ### Step 3: Write Tests First (TDD)
105
+
106
+ Before refactoring, ensure test coverage exists:
107
+
108
+ ```bash
109
+ # Check existing coverage
110
+ bun run test:unit --coverage --collectCoverageFrom='<file-path>'
111
+ ```
112
+
113
+ If no tests exist, write tests that verify current behavior before refactoring.
114
+
115
+ ### Step 4: Implement Extraction
116
+
117
+ For helper functions, see `references/extraction-strategies.md`.
118
+ For full components, use the Container/View pattern skill.
119
+
120
+ ### Step 5: Verify Complexity Resolved
121
+
122
+ ```bash
123
+ bun run lint 2>&1 | grep "cognitive-complexity"
124
+ bun run test:unit
125
+ ```
126
+
127
+ ## Quick Fixes for Common Patterns
128
+
129
+ ### Repeated Conditional Styling
130
+
131
+ **Before (high complexity):**
132
+
133
+ ```tsx
134
+ <Pressable
135
+ style={{
136
+ backgroundColor: selected.includes(item) ? colors.primary : colors.bg,
137
+ borderColor: selected.includes(item) ? colors.primary : colors.border,
138
+ }}
139
+ >
140
+ <Text style={{ color: selected.includes(item) ? "#FFF" : colors.text }}>
141
+ {item}
142
+ </Text>
143
+ </Pressable>
144
+ ```
145
+
146
+ **After (reduced complexity):**
147
+
148
+ ```tsx
149
+ // In Container - pre-compute selection state
150
+ const itemStates = useMemo(
151
+ () =>
152
+ items.map(item => ({
153
+ item,
154
+ isSelected: selected.includes(item),
155
+ })),
156
+ [items, selected]
157
+ );
158
+
159
+ // In View - simple conditional
160
+ <Pressable style={isSelected ? styles.selected : styles.default}>
161
+ <Text style={isSelected ? styles.selectedText : styles.defaultText}>
162
+ {item}
163
+ </Text>
164
+ </Pressable>;
165
+ ```
166
+
167
+ ### Repeated Section Patterns
168
+
169
+ **Before (4x repeated pattern = high complexity):**
170
+
171
+ ```tsx
172
+ {positions.length > 0 && (
173
+ <VStack>
174
+ <Text>Positions</Text>
175
+ <HStack>{positions.map(p => <Chip key={p} ... />)}</HStack>
176
+ </VStack>
177
+ )}
178
+ {tags.length > 0 && (
179
+ <VStack>
180
+ <Text>Tags</Text>
181
+ <HStack>{tags.map(t => <Chip key={t.id} ... />)}</HStack>
182
+ </VStack>
183
+ )}
184
+ // ... repeated 2 more times
185
+ ```
186
+
187
+ **After (extract FilterChipList component):**
188
+
189
+ ```tsx
190
+ <FilterChipList
191
+ title="Positions"
192
+ items={positions}
193
+ selectedItems={filters.positions}
194
+ onToggle={onPositionToggle}
195
+ />
196
+ <FilterChipList
197
+ title="Tags"
198
+ items={tags}
199
+ selectedItems={filters.tags}
200
+ onToggle={onTagToggle}
201
+ />
202
+ ```
203
+
204
+ ### Nested Ternaries
205
+
206
+ **Before:**
207
+
208
+ ```tsx
209
+ {
210
+ isLoading ? (
211
+ <Spinner />
212
+ ) : hasError ? (
213
+ <Error />
214
+ ) : isEmpty ? (
215
+ <Empty />
216
+ ) : (
217
+ <Content />
218
+ );
219
+ }
220
+ ```
221
+
222
+ **After (pre-compute state in Container):**
223
+
224
+ ```tsx
225
+ // Container
226
+ const viewState = useMemo(() => {
227
+ if (isLoading) return "loading";
228
+ if (hasError) return "error";
229
+ if (isEmpty) return "empty";
230
+ return "content";
231
+ }, [isLoading, hasError, isEmpty]);
232
+
233
+ // View - map directly
234
+ const VIEW_STATES = {
235
+ loading: <Spinner />,
236
+ error: <Error />,
237
+ empty: <Empty />,
238
+ content: <Content />,
239
+ } as const;
240
+
241
+ {
242
+ VIEW_STATES[viewState];
243
+ }
244
+ ```
245
+
246
+ ## Reference Documentation
247
+
248
+ For detailed patterns and complete examples:
249
+
250
+ - `references/extraction-strategies.md` - Helper function patterns and when to use each
251
+ - `references/refactoring-patterns.md` - Step-by-step refactoring examples with before/after code
@@ -0,0 +1,456 @@
1
+ # Extraction Strategies
2
+
3
+ This reference provides detailed patterns for extracting code to reduce cognitive complexity.
4
+
5
+ ## Strategy 1: Helper Functions in View Files
6
+
7
+ Helper functions are the simplest extraction strategy. Use them for rendering logic that doesn't need its own component lifecycle.
8
+
9
+ ### Pattern: Render Helper with Props Object
10
+
11
+ Always use a props object (not positional arguments) for type safety and clarity:
12
+
13
+ ```tsx
14
+ /**
15
+ * Renders a section header with title and count.
16
+ * @param props - Helper function properties
17
+ * @param props.title - Section title text
18
+ * @param props.count - Item count to display
19
+ * @param props.colors - Theme colors object
20
+ */
21
+ function renderSectionHeader(props: {
22
+ readonly title: string;
23
+ readonly count: number;
24
+ readonly colors: KanbanColors;
25
+ }) {
26
+ const { title, count, colors } = props;
27
+ return (
28
+ <HStack className="items-center justify-between py-2">
29
+ <Text
30
+ className="text-xs font-semibold uppercase tracking-wider"
31
+ style={{ color: colors.textMuted }}
32
+ >
33
+ {title}
34
+ </Text>
35
+ <Text className="text-xs" style={{ color: colors.textSecondary }}>
36
+ ({count})
37
+ </Text>
38
+ </HStack>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Pattern: Conditional Render Helper
44
+
45
+ For sections that render conditionally based on data:
46
+
47
+ ```tsx
48
+ /**
49
+ * Renders the empty state when no items exist.
50
+ * @param props - Helper function properties
51
+ * @param props.message - Empty state message
52
+ * @param props.colors - Theme colors object
53
+ */
54
+ function renderEmptyState(props: {
55
+ readonly message: string;
56
+ readonly colors: KanbanColors;
57
+ }) {
58
+ const { message, colors } = props;
59
+ return (
60
+ <Box
61
+ className="items-center justify-center p-8"
62
+ style={{ backgroundColor: colors.cardBackground }}
63
+ >
64
+ <Text style={{ color: colors.textMuted }}>{message}</Text>
65
+ </Box>
66
+ );
67
+ }
68
+
69
+ // Usage in View
70
+ const ListView = ({ items, isEmpty, colors }: Props) => (
71
+ <Box>
72
+ {isEmpty
73
+ ? renderEmptyState({ message: "No items found", colors })
74
+ : items.map(item => <Item key={item.id} item={item} />)}
75
+ </Box>
76
+ );
77
+ ```
78
+
79
+ ### Pattern: List Item Render Helper
80
+
81
+ For repeated item rendering with complex styling:
82
+
83
+ ```tsx
84
+ /**
85
+ * Renders a selectable chip/pill item.
86
+ * @param props - Helper function properties
87
+ * @param props.label - Display text for the chip
88
+ * @param props.isSelected - Whether the chip is currently selected
89
+ * @param props.colors - Theme colors for styling
90
+ * @param props.onPress - Callback when chip is pressed
91
+ */
92
+ function renderChip(props: {
93
+ readonly label: string;
94
+ readonly isSelected: boolean;
95
+ readonly colors: KanbanColors;
96
+ readonly onPress: () => void;
97
+ }) {
98
+ const { label, isSelected, colors, onPress } = props;
99
+ return (
100
+ <Pressable
101
+ onPress={onPress}
102
+ style={{
103
+ paddingHorizontal: 10,
104
+ paddingVertical: 5,
105
+ borderRadius: 6,
106
+ backgroundColor: isSelected ? colors.primary : colors.cardBackground,
107
+ borderWidth: 1,
108
+ borderColor: isSelected ? colors.primary : colors.border,
109
+ }}
110
+ >
111
+ <Text
112
+ className="text-xs font-medium"
113
+ style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
114
+ >
115
+ {label}
116
+ </Text>
117
+ </Pressable>
118
+ );
119
+ }
120
+ ```
121
+
122
+ ### When NOT to Use Helper Functions
123
+
124
+ Do not use helper functions when:
125
+
126
+ 1. **The helper would need hooks** - Extract as full component instead
127
+ 2. **The helper is used in multiple files** - Extract as shared component
128
+ 3. **The helper has complex event handling** - Container should handle this
129
+ 4. **The helper exceeds 30 lines** - Consider full component extraction
130
+
131
+ ## Strategy 2: Pre-compute in Container
132
+
133
+ Move complexity from View to Container by pre-computing values.
134
+
135
+ ### Pattern: Selection State Pre-computation
136
+
137
+ **Before (complexity in View):**
138
+
139
+ ```tsx
140
+ // View has repeated .includes() checks
141
+ <Pressable
142
+ style={{
143
+ backgroundColor: filters.positions.includes(position)
144
+ ? colors.primary
145
+ : colors.cardBackground,
146
+ }}
147
+ />
148
+ ```
149
+
150
+ **After (pre-computed in Container):**
151
+
152
+ ```tsx
153
+ // Container
154
+ const positionItems = useMemo(
155
+ () =>
156
+ availablePositions.map(position => ({
157
+ value: position,
158
+ label: position,
159
+ isSelected: filters.positions.includes(position),
160
+ })),
161
+ [availablePositions, filters.positions]
162
+ );
163
+
164
+ // View - simple prop access
165
+ {
166
+ positionItems.map(({ value, label, isSelected }) => (
167
+ <Chip
168
+ key={value}
169
+ label={label}
170
+ isSelected={isSelected}
171
+ onPress={() => onPositionToggle(value)}
172
+ />
173
+ ));
174
+ }
175
+ ```
176
+
177
+ ### Pattern: View State Enumeration
178
+
179
+ **Before (nested ternaries):**
180
+
181
+ ```tsx
182
+ {
183
+ isLoading ? (
184
+ <Spinner />
185
+ ) : error ? (
186
+ <ErrorView error={error} />
187
+ ) : data.length === 0 ? (
188
+ <EmptyState />
189
+ ) : (
190
+ <DataList data={data} />
191
+ );
192
+ }
193
+ ```
194
+
195
+ **After (enumerated state):**
196
+
197
+ ```tsx
198
+ // Container
199
+ type ViewState = "loading" | "error" | "empty" | "ready";
200
+
201
+ const viewState = useMemo((): ViewState => {
202
+ if (isLoading) return "loading";
203
+ if (error) return "error";
204
+ if (data.length === 0) return "empty";
205
+ return "ready";
206
+ }, [isLoading, error, data.length]);
207
+
208
+ // View - switch or map
209
+ const ComponentView = ({ viewState, data, error }: Props) => (
210
+ <Box>
211
+ {viewState === "loading" && <Spinner />}
212
+ {viewState === "error" && <ErrorView error={error} />}
213
+ {viewState === "empty" && <EmptyState />}
214
+ {viewState === "ready" && <DataList data={data} />}
215
+ </Box>
216
+ );
217
+ ```
218
+
219
+ ### Pattern: Style Pre-computation
220
+
221
+ **Before (inline style objects create complexity and perf issues):**
222
+
223
+ ```tsx
224
+ <Box
225
+ style={{
226
+ backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
227
+ padding: isCompact ? 8 : 16,
228
+ borderRadius: isRounded ? 12 : 0,
229
+ }}
230
+ />
231
+ ```
232
+
233
+ **After (computed style in Container):**
234
+
235
+ ```tsx
236
+ // Container
237
+ const boxStyle = useMemo(
238
+ () => ({
239
+ backgroundColor: isDark ? colors.dark.bg : colors.light.bg,
240
+ padding: isCompact ? 8 : 16,
241
+ borderRadius: isRounded ? 12 : 0,
242
+ }),
243
+ [isDark, isCompact, isRounded, colors]
244
+ );
245
+
246
+ // View
247
+ <Box style={boxStyle} />;
248
+ ```
249
+
250
+ ## Strategy 3: Full Component Extraction
251
+
252
+ When helper functions aren't sufficient, extract a full Container/View component.
253
+
254
+ ### Decision Checklist
255
+
256
+ Extract as full component when 3+ of these apply:
257
+
258
+ - [ ] Pattern repeats in 2+ files
259
+ - [ ] Section needs its own state
260
+ - [ ] Section has 4+ props
261
+ - [ ] Section has callbacks that could be simplified
262
+ - [ ] Section represents a meaningful domain concept
263
+ - [ ] Section could be tested independently
264
+
265
+ ### Extraction Steps
266
+
267
+ 1. **Identify the repeated/complex pattern**
268
+ 2. **Define the component's props interface**
269
+ 3. **Create directory structure:**
270
+ ```text
271
+ ComponentName/
272
+ ├── ComponentNameContainer.tsx
273
+ ├── ComponentNameView.tsx
274
+ └── index.tsx
275
+ ```
276
+ 4. **Write Container with logic/state**
277
+ 5. **Write View with pure rendering**
278
+ 6. **Update parent to use new component**
279
+ 7. **Write tests for new component**
280
+
281
+ ### Example: Extracting FilterChipList
282
+
283
+ **Before (in FilterModalView.tsx):**
284
+
285
+ ```tsx
286
+ {
287
+ /* Positions - repeated pattern */
288
+ }
289
+ {
290
+ availablePositions.length > 0 && (
291
+ <VStack className="gap-3">
292
+ <Text style={{ color: colors.textMuted }}>Positions</Text>
293
+ <HStack className="flex-wrap gap-2">
294
+ {availablePositions.map(position => (
295
+ <Pressable
296
+ key={position}
297
+ onPress={() => onPositionToggle(position)}
298
+ style={{
299
+ backgroundColor: filters.positions.includes(position)
300
+ ? colors.primary
301
+ : colors.cardBackground,
302
+ // ... more styles
303
+ }}
304
+ >
305
+ <Text>{position}</Text>
306
+ </Pressable>
307
+ ))}
308
+ </HStack>
309
+ </VStack>
310
+ );
311
+ }
312
+
313
+ {
314
+ /* Tags - same pattern with slight variation */
315
+ }
316
+ {
317
+ availableTags.length > 0 && (
318
+ <VStack className="gap-3">
319
+ <Text style={{ color: colors.textMuted }}>Tags</Text>
320
+ <HStack className="flex-wrap gap-2">
321
+ {availableTags.map(tag => (
322
+ <Pressable
323
+ key={tag.id}
324
+ onPress={() => onTagToggle(tag.id)}
325
+ // ... same pattern
326
+ >
327
+ <Text>{tag.name}</Text>
328
+ </Pressable>
329
+ ))}
330
+ </HStack>
331
+ </VStack>
332
+ );
333
+ }
334
+ ```
335
+
336
+ **After (FilterChipList component):**
337
+
338
+ ```tsx
339
+ // FilterChipListContainer.tsx
340
+ interface FilterChipListProps {
341
+ readonly title: string;
342
+ readonly items: readonly { id: string; label: string; color?: string }[];
343
+ readonly selectedIds: readonly string[];
344
+ readonly colors: KanbanColors;
345
+ readonly onToggle: (id: string) => void;
346
+ }
347
+
348
+ const FilterChipListContainer = ({
349
+ title,
350
+ items,
351
+ selectedIds,
352
+ colors,
353
+ onToggle,
354
+ }: FilterChipListProps) => {
355
+ const chipItems = useMemo(
356
+ () =>
357
+ items.map(item => ({
358
+ ...item,
359
+ isSelected: selectedIds.includes(item.id),
360
+ })),
361
+ [items, selectedIds]
362
+ );
363
+
364
+ const handleToggle = useCallback(
365
+ (id: string) => () => onToggle(id),
366
+ [onToggle]
367
+ );
368
+
369
+ return (
370
+ <FilterChipListView
371
+ title={title}
372
+ items={chipItems}
373
+ colors={colors}
374
+ onToggle={handleToggle}
375
+ />
376
+ );
377
+ };
378
+ ```
379
+
380
+ ```tsx
381
+ // FilterChipListView.tsx
382
+ interface FilterChipListViewProps {
383
+ readonly title: string;
384
+ readonly items: readonly {
385
+ id: string;
386
+ label: string;
387
+ color?: string;
388
+ isSelected: boolean;
389
+ }[];
390
+ readonly colors: KanbanColors;
391
+ readonly onToggle: (id: string) => () => void;
392
+ }
393
+
394
+ const FilterChipListView = ({
395
+ title,
396
+ items,
397
+ colors,
398
+ onToggle,
399
+ }: FilterChipListViewProps) => (
400
+ <VStack className="gap-3">
401
+ <Text
402
+ className="text-xs font-semibold uppercase tracking-wider"
403
+ style={{ color: colors.textMuted }}
404
+ >
405
+ {title}
406
+ </Text>
407
+ <HStack className="flex-wrap gap-2">
408
+ {items.map(({ id, label, color, isSelected }) => (
409
+ <Pressable
410
+ key={id}
411
+ onPress={onToggle(id)}
412
+ style={{
413
+ paddingHorizontal: 10,
414
+ paddingVertical: 5,
415
+ borderRadius: 6,
416
+ backgroundColor: isSelected
417
+ ? (color ?? colors.primary)
418
+ : colors.cardBackground,
419
+ borderWidth: 1,
420
+ borderColor: isSelected ? (color ?? colors.primary) : colors.border,
421
+ }}
422
+ >
423
+ <Text
424
+ className="text-xs font-medium"
425
+ style={{ color: isSelected ? "#FFFFFF" : colors.textSecondary }}
426
+ >
427
+ {label}
428
+ </Text>
429
+ </Pressable>
430
+ ))}
431
+ </HStack>
432
+ </VStack>
433
+ );
434
+
435
+ FilterChipListView.displayName = "FilterChipListView";
436
+ export default memo(FilterChipListView);
437
+ ```
438
+
439
+ **Usage in parent:**
440
+
441
+ ```tsx
442
+ <FilterChipList
443
+ title="Positions"
444
+ items={positionItems}
445
+ selectedIds={filters.positions}
446
+ colors={colors}
447
+ onToggle={onPositionToggle}
448
+ />
449
+ <FilterChipList
450
+ title="Tags"
451
+ items={tagItems}
452
+ selectedIds={filters.tags}
453
+ colors={colors}
454
+ onToggle={onTagToggle}
455
+ />
456
+ ```