|
| 1 | +--- |
| 2 | +name: react-generic-components |
| 3 | +description: Guidelines for building truly generic and reusable React components without business logic or specific use-case code in React/TypeScript codebases |
| 4 | +license: MIT |
| 5 | +allowed-tools: |
| 6 | + - read |
| 7 | + - write |
| 8 | + - edit |
| 9 | + - grep |
| 10 | +metadata: |
| 11 | + version: "1.0" |
| 12 | + author: "KernelCI Dashboard Team" |
| 13 | + tags: ["react", "components", "architecture", "reusability"] |
| 14 | +--- |
| 15 | + |
| 16 | +# React Generic Components Skill |
| 17 | + |
| 18 | +This skill ensures React components remain truly generic and reusable by avoiding hardcoded business logic or use-case-specific code. |
| 19 | + |
| 20 | +## Core Principle |
| 21 | + |
| 22 | +**Generic components should not contain specific logic for any particular use case.** |
| 23 | + |
| 24 | +Components should accept configuration through: |
| 25 | +- Props |
| 26 | +- Configuration objects |
| 27 | +- Context (when appropriate) |
| 28 | +- Composition patterns |
| 29 | + |
| 30 | +## Anti-Patterns to Avoid |
| 31 | + |
| 32 | +### ❌ Hardcoded Business Logic |
| 33 | + |
| 34 | +```typescript |
| 35 | +// ❌ BAD - Checking for specific values in generic component |
| 36 | +const TableCell = ({ cell }) => { |
| 37 | + const dataTestId = cell.column.id === 'details' |
| 38 | + ? 'details-button' |
| 39 | + : undefined; |
| 40 | + |
| 41 | + return <td data-test-id={dataTestId}>...</td>; |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +**Problem**: This component knows about a specific column ID ("details"). It's no longer generic. |
| 46 | + |
| 47 | +### ❌ Conditional Rendering Based on Specific Values |
| 48 | + |
| 49 | +```typescript |
| 50 | +// ❌ BAD - Special cases for specific data |
| 51 | +const StatusBadge = ({ status }) => { |
| 52 | + if (status === 'COMPLETED') { |
| 53 | + return <GreenBadge>Done!</GreenBadge>; |
| 54 | + } |
| 55 | + if (status === 'FAILED') { |
| 56 | + return <RedBadge>Error!</RedBadge>; |
| 57 | + } |
| 58 | + return <Badge>{status}</Badge>; |
| 59 | +}; |
| 60 | +``` |
| 61 | + |
| 62 | +**Problem**: Every new status requires modifying the component. Not scalable. |
| 63 | + |
| 64 | +### ❌ Mixed Concerns |
| 65 | + |
| 66 | +```typescript |
| 67 | +// ❌ BAD - Component handles both rendering and business logic |
| 68 | +const ChartLegend = ({ data }) => { |
| 69 | + // Business logic mixed in |
| 70 | + if (data.value === 0 && data.label !== 'important') { |
| 71 | + return null; |
| 72 | + } |
| 73 | + |
| 74 | + return <div>...</div>; |
| 75 | +}; |
| 76 | +``` |
| 77 | + |
| 78 | +**Problem**: Generic chart component shouldn't decide what to hide based on business rules. |
| 79 | + |
| 80 | +## Correct Patterns |
| 81 | + |
| 82 | +### ✅ Configuration Through Props |
| 83 | + |
| 84 | +```typescript |
| 85 | +// ✅ GOOD - Generic, accepts configuration |
| 86 | +interface TableCellProps { |
| 87 | + children: React.ReactNode; |
| 88 | + dataTestId?: string; |
| 89 | + linkProps?: LinkProps; |
| 90 | +} |
| 91 | + |
| 92 | +const TableCell = ({ children, dataTestId, linkProps }: TableCellProps) => { |
| 93 | + return ( |
| 94 | + <td> |
| 95 | + <Link {...linkProps} data-test-id={dataTestId}> |
| 96 | + {children} |
| 97 | + </Link> |
| 98 | + </td> |
| 99 | + ); |
| 100 | +}; |
| 101 | +``` |
| 102 | + |
| 103 | +**Usage**: |
| 104 | +```typescript |
| 105 | +// Configuration happens at usage site |
| 106 | +<TableCell |
| 107 | + dataTestId="details-button" |
| 108 | + linkProps={detailsLink} |
| 109 | +> |
| 110 | + {content} |
| 111 | +</TableCell> |
| 112 | +``` |
| 113 | + |
| 114 | +### ✅ Configuration Through Metadata |
| 115 | + |
| 116 | +```typescript |
| 117 | +// ✅ GOOD - Read configuration from metadata |
| 118 | +const TableCell = ({ cell }) => { |
| 119 | + // Generic: reads from column's configuration |
| 120 | + const metaResult = ColumnMetaSchema.safeParse(cell.column.columnDef.meta); |
| 121 | + const dataTestId = metaResult.success ? metaResult.data.dataTestId : undefined; |
| 122 | + |
| 123 | + return <td data-test-id={dataTestId}>...</td>; |
| 124 | +}; |
| 125 | +``` |
| 126 | + |
| 127 | +**Configuration** (separate from component): |
| 128 | +```typescript |
| 129 | +// Column definition includes metadata |
| 130 | +const columns = [ |
| 131 | + { |
| 132 | + id: 'details', |
| 133 | + header: () => <DetailsHeader />, |
| 134 | + cell: () => <DetailsIcon />, |
| 135 | + meta: { |
| 136 | + dataTestId: 'details-button', // ← Configuration |
| 137 | + }, |
| 138 | + }, |
| 139 | +]; |
| 140 | +``` |
| 141 | + |
| 142 | +### ✅ Mapping Objects for Status/Variants |
| 143 | + |
| 144 | +```typescript |
| 145 | +// ✅ GOOD - Configuration-driven |
| 146 | +interface StatusBadgeProps { |
| 147 | + status: string; |
| 148 | + colorMap: Record<string, string>; |
| 149 | + labelMap?: Record<string, string>; |
| 150 | +} |
| 151 | + |
| 152 | +const StatusBadge = ({ status, colorMap, labelMap }: StatusBadgeProps) => { |
| 153 | + const color = colorMap[status] || 'gray'; |
| 154 | + const label = labelMap?.[status] || status; |
| 155 | + |
| 156 | + return <Badge color={color}>{label}</Badge>; |
| 157 | +}; |
| 158 | +``` |
| 159 | + |
| 160 | +**Usage**: |
| 161 | +```typescript |
| 162 | +const STATUS_COLORS = { |
| 163 | + COMPLETED: 'green', |
| 164 | + FAILED: 'red', |
| 165 | + PENDING: 'yellow', |
| 166 | +}; |
| 167 | + |
| 168 | +<StatusBadge status={item.status} colorMap={STATUS_COLORS} /> |
| 169 | +``` |
| 170 | + |
| 171 | +### ✅ Generic with Label-Based IDs |
| 172 | + |
| 173 | +```typescript |
| 174 | +// ✅ GOOD - Uses generic property from data |
| 175 | +const ChartLegend = ({ chartValues, onClick }) => { |
| 176 | + return chartValues.map(value => ( |
| 177 | + <button |
| 178 | + key={value.color} |
| 179 | + onClick={() => onClick?.(value.label)} |
| 180 | + data-test-id={value.label} // Generic: uses existing property |
| 181 | + > |
| 182 | + <ColorCircle color={value.color} /> |
| 183 | + <span>{value.value}</span> |
| 184 | + <span>{value.label}</span> |
| 185 | + </button> |
| 186 | + )); |
| 187 | +}; |
| 188 | +``` |
| 189 | + |
| 190 | +## Real-World Example from Codebase |
| 191 | + |
| 192 | +### Before (Specific Logic in Generic Component) |
| 193 | + |
| 194 | +```typescript |
| 195 | +// ❌ BAD |
| 196 | +const TableCellComponent = ({ cell }) => { |
| 197 | + const dataTestId = |
| 198 | + cell.column.id === DETAILS_COLUMN_ID ? 'details-button' : undefined; |
| 199 | + |
| 200 | + return <TableCellWithLink dataTestId={dataTestId}>...</TableCellWithLink>; |
| 201 | +}; |
| 202 | +``` |
| 203 | + |
| 204 | +### After (Generic Configuration-Based) |
| 205 | + |
| 206 | +```typescript |
| 207 | +// ✅ GOOD - Generic component |
| 208 | +const ColumnMetaSchema = z.object({ |
| 209 | + dataTestId: z.string().optional(), |
| 210 | +}); |
| 211 | + |
| 212 | +const TableCellComponent = ({ cell }) => { |
| 213 | + const metaResult = ColumnMetaSchema.safeParse(cell.column.columnDef.meta); |
| 214 | + const dataTestId = metaResult.success ? metaResult.data.dataTestId : undefined; |
| 215 | + |
| 216 | + return <TableCellWithLink dataTestId={dataTestId}>...</TableCellWithLink>; |
| 217 | +}; |
| 218 | + |
| 219 | +// Configuration (separate from component) |
| 220 | +const buildColumns = [ |
| 221 | + { |
| 222 | + id: DETAILS_COLUMN_ID, |
| 223 | + header: () => <DetailsHeader />, |
| 224 | + cell: () => <DetailsIcon />, |
| 225 | + meta: { dataTestId: 'details-button' }, |
| 226 | + }, |
| 227 | +]; |
| 228 | +``` |
| 229 | + |
| 230 | +## Decision Tree for AI Agents |
| 231 | + |
| 232 | +When adding functionality to a component: |
| 233 | + |
| 234 | +``` |
| 235 | +Is this component used in multiple places? |
| 236 | +├─ Yes → Make it generic |
| 237 | +│ └─ Can the behavior be configured? |
| 238 | +│ ├─ Yes → Use props/metadata |
| 239 | +│ └─ No → Consider composition or separate components |
| 240 | +└─ No → Specific logic may be acceptable |
| 241 | +``` |
| 242 | + |
| 243 | +## Instructions for AI Agents |
| 244 | + |
| 245 | +1. **Before modifying a component**: |
| 246 | + - Check if it's used in multiple places (`grep` or search) |
| 247 | + - Look for patterns like "Generic", "Reusable", or "Common" in file paths |
| 248 | + - Check if it's in a `components/ui/` or similar directory |
| 249 | + |
| 250 | +2. **If the component is generic**: |
| 251 | + - Do NOT add hardcoded checks for specific values |
| 252 | + - Do NOT add business logic |
| 253 | + - Instead, add a prop or use configuration |
| 254 | + |
| 255 | +3. **Choose the right approach**: |
| 256 | + - Simple cases → Add a prop |
| 257 | + - Complex cases → Use metadata/configuration objects |
| 258 | + - When you need the existing data property → Use that (like `label`, `id`, etc.) |
| 259 | + |
| 260 | +4. **Verify your solution**: |
| 261 | + - Can this component work for ANY use case, not just the current one? |
| 262 | + - Is the configuration separate from the component? |
| 263 | + - Would adding a new variant require changing the component? |
| 264 | + - If yes → Refactor to be more generic |
| 265 | + |
| 266 | +## Benefits |
| 267 | + |
| 268 | +- **Reusability**: One component, infinite use cases |
| 269 | +- **Maintainability**: Changes happen in configuration, not component code |
| 270 | +- **Testability**: Generic components are easier to test |
| 271 | +- **Scalability**: New features don't require component changes |
| 272 | +- **Type Safety**: Configuration can be validated with Zod |
| 273 | + |
| 274 | +## References |
| 275 | + |
| 276 | +- Real implementation: `src/components/Table/TableComponents.tsx` |
| 277 | +- Configuration examples: `src/components/BuildsTable/DefaultBuildsColumns.tsx` |
| 278 | +- Related skill: `type-safety` (for validating configuration) |
0 commit comments