dev-booster 1.18.0 → 1.18.1
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.
- package/package.json +1 -1
- package/template/.devbooster/MANIFEST.md +34 -1
- package/template/.devbooster/boosters/advisor.md +5 -0
- package/template/.devbooster/boosters/audit.md +20 -0
- package/template/.devbooster/boosters/auto-triage.md +5 -0
- package/template/.devbooster/boosters/backend.md +12 -0
- package/template/.devbooster/boosters/builder.md +5 -0
- package/template/.devbooster/boosters/code-audit.md +19 -0
- package/template/.devbooster/boosters/coder.md +5 -0
- package/template/.devbooster/boosters/create.md +5 -0
- package/template/.devbooster/boosters/debug.md +19 -0
- package/template/.devbooster/boosters/deploy.md +12 -0
- package/template/.devbooster/boosters/discovery.md +5 -0
- package/template/.devbooster/boosters/frontend.md +12 -0
- package/template/.devbooster/boosters/implementation.md +5 -0
- package/template/.devbooster/boosters/investigation.md +5 -0
- package/template/.devbooster/boosters/performance.md +12 -0
- package/template/.devbooster/boosters/planning.md +5 -0
- package/template/.devbooster/boosters/refactor.md +12 -0
- package/template/.devbooster/boosters/review.md +12 -0
- package/template/.devbooster/boosters/security.md +14 -0
- package/template/.devbooster/boosters/smart-task.md +27 -16
- package/template/.devbooster/boosters/stack-refresh.md +20 -0
- package/template/.devbooster/boosters/testing.md +12 -0
- package/template/.devbooster/hub/knowledge/angular-patterns.md +185 -0
- package/template/.devbooster/hub/knowledge/dependency-guide.md +175 -0
- package/template/.devbooster/hub/knowledge/eslint-migration.md +206 -0
- package/template/.devbooster/hub/knowledge/index.md +91 -0
- package/template/.devbooster/hub/knowledge/migration-guides.md +137 -0
- package/template/.devbooster/hub/knowledge/monorepo-patterns.md +121 -0
- package/template/.devbooster/hub/knowledge/nestjs-patterns.md +185 -0
- package/template/.devbooster/hub/knowledge/nextjs-pitfalls.md +226 -0
- package/template/.devbooster/hub/knowledge/nodejs-patterns.md +148 -0
- package/template/.devbooster/hub/knowledge/package-manager-patterns.md +143 -0
- package/template/.devbooster/hub/knowledge/prisma-postgresql-patterns.md +188 -0
- package/template/.devbooster/hub/knowledge/react-patterns.md +500 -0
- package/template/.devbooster/hub/knowledge/tailwind-shadcn-patterns.md +146 -0
- package/template/.devbooster/hub/knowledge/tanstack-patterns.md +278 -0
- package/template/.devbooster/hub/knowledge/testing-patterns.md +164 -0
- package/template/.devbooster/hub/knowledge/trpc-patterns.md +212 -0
- package/template/.devbooster/hub/knowledge/typescript-patterns.md +219 -0
- package/template/.devbooster/hub/knowledge/upgrade-fallout.md +154 -0
- package/template/.devbooster/hub/knowledge/vite-patterns.md +177 -0
- package/template/.devbooster/rules/GUIDE.md +24 -0
- package/template/.devbooster/rules/PROTOCOL.md +3 -2
- package/template/.devbooster/rules/TRIGGERS.md +14 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# ⚛️ React Patterns
|
|
2
|
+
|
|
3
|
+
> **Purpose:** Problematic patterns and fixes for React 19 + React Hooks
|
|
4
|
+
> **Primary official sources:** [React documentation](https://react.dev) · [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) · [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
|
5
|
+
|
|
6
|
+
## Project Convention Decision Rule
|
|
7
|
+
|
|
8
|
+
Before applying this guidance, verify the installed versions, local rules, rendering model, existing component and hook abstractions, data-fetching conventions, and tests. Preserve a valid established project convention; do not replace it only because another documented approach is also valid. Use official sources to verify API behavior, compatibility, constraints, and migrations. Recommend a change only when the developer requests it or evidence shows the current approach is incompatible, unsafe, deprecated, broken, or responsible for a verified issue.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Index
|
|
13
|
+
|
|
14
|
+
1. [Components Created During Render](#components-created-during-render)
|
|
15
|
+
2. [Decorative useMemo](#decorative-usememo)
|
|
16
|
+
3. [Initial State from localStorage](#initial-state-from-localstorage)
|
|
17
|
+
4. [Fetching Data in Effects](#fetching-data-in-effects)
|
|
18
|
+
5. [Sequential Fetches with State Dependencies](#sequential-fetches-with-state-dependencies)
|
|
19
|
+
6. [Legitimate Fetch in an Effect](#legitimate-fetch-in-an-effect)
|
|
20
|
+
7. [Derived State That Should Be Computed](#derived-state-that-should-be-computed)
|
|
21
|
+
8. [Lazy State Initializer](#lazy-state-initializer)
|
|
22
|
+
9. [Key to Reset State](#key-to-reset-state)
|
|
23
|
+
10. [Effect with Two Responsibilities](#effect-with-two-responsibilities)
|
|
24
|
+
11. [Keyed Animation Reset](#keyed-animation-reset)
|
|
25
|
+
12. [Function Placement Around Effects](#function-placement-around-effects)
|
|
26
|
+
13. [exhaustive-deps Suppressions](#exhaustive-deps-suppressions)
|
|
27
|
+
14. [useRef Without Initial Value in React 19](#useref-without-initial-value-in-react-19)
|
|
28
|
+
15. [State Mutation](#state-mutation)
|
|
29
|
+
16. [Choose an Async UI Strategy Before Adding an Effect](#choose-an-async-ui-strategy-before-adding-an-effect)
|
|
30
|
+
17. [Suspense Boundaries Are Not Data Fetchers](#suspense-boundaries-are-not-data-fetchers)
|
|
31
|
+
18. [Extract a Custom Hook Only for a Reusable Stateful Contract](#extract-a-custom-hook-only-for-a-reusable-stateful-contract)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Components Created During Render
|
|
36
|
+
|
|
37
|
+
### Problem
|
|
38
|
+
Defining a React component inside the body of another component. On each render, a new component function/type is created. Because its identity changes, React unmounts and remounts that subtree, losing local state and defeating memoization.
|
|
39
|
+
|
|
40
|
+
*Source: [react.dev — Keeping Components Pure](https://react.dev/learn/keeping-components-pure)*
|
|
41
|
+
|
|
42
|
+
### Code (symptom)
|
|
43
|
+
```tsx
|
|
44
|
+
const Card = () => {
|
|
45
|
+
const Icon = () => <svg>{/* ... */}</svg> // ← RECREATED ON EVERY RENDER
|
|
46
|
+
return <Icon />
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Fix
|
|
51
|
+
```tsx
|
|
52
|
+
// Option 1: Inline the element (simplest)
|
|
53
|
+
const Card = () => <svg>{/* ... */}</svg>
|
|
54
|
+
|
|
55
|
+
// Option 2: Extract outside the component
|
|
56
|
+
const Icon = () => <svg>{/* ... */}</svg>
|
|
57
|
+
const Card = () => <Icon />
|
|
58
|
+
|
|
59
|
+
// Option 3: Separate file (when reusable)
|
|
60
|
+
```
|
|
61
|
+
### When NOT to apply
|
|
62
|
+
- Rare — in practice, inline or extract always resolves it
|
|
63
|
+
- If the subcomponent needs many props from the parent scope, consider passing it as `children`
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Decorative useMemo
|
|
68
|
+
|
|
69
|
+
### Problem
|
|
70
|
+
`useMemo` wrapping trivial computations where the cost of the hook (dependency comparison + closure allocation) is greater than the computation itself.
|
|
71
|
+
|
|
72
|
+
*Source: [react.dev — useMemo](https://react.dev/reference/react/useMemo)*
|
|
73
|
+
|
|
74
|
+
> "You should only rely on useMemo as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first."
|
|
75
|
+
|
|
76
|
+
### Code (symptom)
|
|
77
|
+
```tsx
|
|
78
|
+
const colorClass = useMemo(() => {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case 'active': return 'text-green-600'
|
|
81
|
+
case 'inactive': return 'text-red-600'
|
|
82
|
+
}
|
|
83
|
+
}, [status])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Fix
|
|
87
|
+
```tsx
|
|
88
|
+
const colorClass = status === 'active' ? 'text-green-600' : 'text-red-600'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### When to keep `useMemo`
|
|
92
|
+
- Profiling shows that the calculation is meaningfully expensive
|
|
93
|
+
- A stable reference is required by a memoized child and avoids a measured re-render
|
|
94
|
+
|
|
95
|
+
Do not use `useMemo` merely because a calculation contains an array or several lines. For Effects, prefer removing unnecessary object or function dependencies—for example, create them inside the Effect—rather than memoizing solely to control when an Effect runs.
|
|
96
|
+
|
|
97
|
+
### Note on React Compiler
|
|
98
|
+
The React Compiler can automatically memoize eligible values and functions. It does not make every existing manual memo safe to remove: confirm the compiler is enabled for the code and validate behavior and performance before changing manual memoization.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Initial State from localStorage
|
|
103
|
+
|
|
104
|
+
### Problem
|
|
105
|
+
Using an Effect only to read a synchronous browser value such as `localStorage` adds a render after mount in client-only applications.
|
|
106
|
+
|
|
107
|
+
*Source: [react.dev — useState: Avoiding recreating the initial state](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state)*
|
|
108
|
+
|
|
109
|
+
### Fix for client-only rendering
|
|
110
|
+
```tsx
|
|
111
|
+
const [data, setData] = useState(() => {
|
|
112
|
+
try {
|
|
113
|
+
const stored = localStorage.getItem('config')
|
|
114
|
+
return stored ? JSON.parse(stored) : []
|
|
115
|
+
} catch {
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### SSR caveat
|
|
122
|
+
`typeof window === 'undefined'` prevents a server-side exception, but it can still produce different initial server and client output when persisted data exists. For SSR, use the same initial value on server and client, or isolate the browser-only state behind a client-only boundary; hydrate persisted state after mount when matching initial HTML is required.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Fetching Data in Effects
|
|
127
|
+
|
|
128
|
+
### Problem
|
|
129
|
+
Fetching in an Effect is supported when a component must synchronize with an external system, but it needs complete dependencies and stale-response handling. A loop occurs only when an Effect changes a dependency to a new value that makes the Effect run again—not simply because it calls a state setter.
|
|
130
|
+
|
|
131
|
+
*Source: [react.dev — Fetching data with Effects](https://react.dev/reference/react/useEffect#fetching-data-with-effects)*
|
|
132
|
+
|
|
133
|
+
### Dependency-correct example
|
|
134
|
+
```tsx
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const controller = new AbortController()
|
|
137
|
+
|
|
138
|
+
async function loadDetails() {
|
|
139
|
+
setLoading(true)
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(`/api/items/${selectedId}`, {
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
})
|
|
144
|
+
if (!response.ok) throw new Error('Failed to load item')
|
|
145
|
+
const details = await response.json()
|
|
146
|
+
if (!controller.signal.aborted) setDetails(details)
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!controller.signal.aborted) setError(error)
|
|
149
|
+
} finally {
|
|
150
|
+
if (!controller.signal.aborted) setLoading(false)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (selectedId) void loadDetails()
|
|
155
|
+
return () => controller.abort()
|
|
156
|
+
}, [selectedId])
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
If an initial request selects an ID, keep that request separate from the details Effect; the details Effect can then react to `selectedId` without also changing it. Define async helpers inside the Effect when they are only used there, so their dependencies are explicit.
|
|
160
|
+
|
|
161
|
+
### Architectural choice
|
|
162
|
+
Effect-based fetching is valid, but it does not provide caching, request deduplication, or server preloading by itself. Prefer framework-integrated data loading or a caching library when those capabilities are needed.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Sequential Fetches with State Dependencies
|
|
167
|
+
|
|
168
|
+
**Evidence:** Field-validated in a real audit.
|
|
169
|
+
|
|
170
|
+
### Problem
|
|
171
|
+
One request updates state required by a second request, while both requests are placed in the same Effect that depends on that state. This can re-run the first request after it changes the dependency and create repeated requests or a loop.
|
|
172
|
+
|
|
173
|
+
### Code (symptom)
|
|
174
|
+
```tsx
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
fetchPrimaryData() // updates selectedId
|
|
177
|
+
fetchDetails() // reads selectedId
|
|
178
|
+
}, [selectedId])
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Fix
|
|
182
|
+
Separate the responsibilities so that the second Effect reacts to the value produced by the first:
|
|
183
|
+
```tsx
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
void fetchPrimaryData()
|
|
186
|
+
}, [])
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (selectedId) void fetchDetails(selectedId)
|
|
190
|
+
}, [selectedId])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If both requests must run exactly once as one workflow, keep the selected value local to a single async function instead of using state as the handoff.
|
|
194
|
+
|
|
195
|
+
### Verify first
|
|
196
|
+
Confirm whether `selectedId` is intentionally refreshed later. If it is, use complete dependencies, stale-response protection, and a deliberate refresh strategy rather than an empty dependency array.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Legitimate Fetch in an Effect
|
|
201
|
+
|
|
202
|
+
**Evidence:** Field-validated in a real audit.
|
|
203
|
+
|
|
204
|
+
### Context
|
|
205
|
+
A fetch on mount is not automatically an error. It can be the correct way to synchronize visible client UI with a network resource, browser API, or another external system.
|
|
206
|
+
|
|
207
|
+
*Source: [react.dev — Fetching data with Effects](https://react.dev/reference/react/useEffect#fetching-data-with-effects)*
|
|
208
|
+
|
|
209
|
+
### Decision guidance
|
|
210
|
+
Keep the Effect when the data must be synchronized while the component is visible and no framework-level data-loading path is available. The Effect still needs complete dependencies, loading/error handling, and protection against stale responses.
|
|
211
|
+
|
|
212
|
+
For server-rendered applications or data that benefits from caching, preloading, or request deduplication, prefer the framework's data-loading mechanism or a client cache. This is an architectural choice, not an automatic lint fix.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Derived State That Should Be Computed
|
|
217
|
+
|
|
218
|
+
### Problem
|
|
219
|
+
State that is purely derived from props or other state, but is being synchronized via `useEffect` instead of being computed directly.
|
|
220
|
+
|
|
221
|
+
*Source: [react.dev — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)*
|
|
222
|
+
|
|
223
|
+
> "When something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering."
|
|
224
|
+
|
|
225
|
+
### Code (symptom)
|
|
226
|
+
```tsx
|
|
227
|
+
const [isActive, setIsActive] = useState(false)
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
setIsActive(statusCode === 200)
|
|
230
|
+
}, [statusCode])
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Fix
|
|
234
|
+
```tsx
|
|
235
|
+
const isActive = statusCode === 200
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### When not to apply
|
|
239
|
+
- The value needs to be set independently (it is not purely derived)
|
|
240
|
+
|
|
241
|
+
An expensive derived value should still be calculated from existing state or props, optionally with `useMemo` when profiling justifies it; it should not be synchronized into separate state.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Lazy State Initializer
|
|
246
|
+
|
|
247
|
+
### Problem
|
|
248
|
+
Initial state calculation function being called on every render, even though the result is only used on mount.
|
|
249
|
+
|
|
250
|
+
*Source: [react.dev — useState: Avoiding recreating the initial state](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state)*
|
|
251
|
+
|
|
252
|
+
### Code (symptom)
|
|
253
|
+
```tsx
|
|
254
|
+
const [items, setItems] = useState(createInitialItems()) // called on every render
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Fix
|
|
258
|
+
```tsx
|
|
259
|
+
const [items, setItems] = useState(createInitialItems) // passing the function, not the result
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### When to use
|
|
263
|
+
- Expensive initial computation (large arrays, complex transformations)
|
|
264
|
+
- Reading from `localStorage` / `sessionStorage` on mount
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Key to Reset State
|
|
269
|
+
|
|
270
|
+
### Problem
|
|
271
|
+
Effect resetting internal state when props change, typically in modals, forms, or tabs.
|
|
272
|
+
|
|
273
|
+
*Source: [react.dev — useState: Resetting state with a key](https://react.dev/reference/react/useState#resetting-state-with-a-key)*
|
|
274
|
+
|
|
275
|
+
### Code (symptom)
|
|
276
|
+
```tsx
|
|
277
|
+
<Profile userId={userId} />
|
|
278
|
+
// useEffect inside Profile: if (prev.userId !== userId) { setState(initial) }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Fix
|
|
282
|
+
```tsx
|
|
283
|
+
<Profile key={userId} userId={userId} />
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Why it works
|
|
287
|
+
React unmounts/remounts the component when the `key` changes. Local state starts fresh without an Effect.
|
|
288
|
+
|
|
289
|
+
### Caveat
|
|
290
|
+
A key reset also runs cleanup and resets the entire subtree, including focus, pending work, and every local state value. Use it when a full reset is intentional.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Effect with Two Responsibilities
|
|
295
|
+
|
|
296
|
+
### Problem
|
|
297
|
+
A single `useEffect` managing unrelated responsibilities (for example, navigation and data fetching) is harder to reason about. Splitting it must preserve each responsibility's original trigger.
|
|
298
|
+
|
|
299
|
+
*Source: [react.dev — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)*
|
|
300
|
+
|
|
301
|
+
### Fix
|
|
302
|
+
```tsx
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
setScreen('details')
|
|
305
|
+
}, [itemId]) // preserves the original behavior
|
|
306
|
+
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
async function load() {
|
|
309
|
+
const response = await fetch(`/api/items/${itemId}`)
|
|
310
|
+
setData(await response.json())
|
|
311
|
+
}
|
|
312
|
+
void load()
|
|
313
|
+
}, [itemId])
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
When navigation is caused by a user action, prefer updating screen state in that event handler. If the actions must occur in a specific order, keep that orchestration in the event or a single deliberate workflow instead of relying on Effect ordering.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Keyed Animation Reset
|
|
321
|
+
|
|
322
|
+
### Problem
|
|
323
|
+
An animation with local progress may need to restart when an index changes.
|
|
324
|
+
|
|
325
|
+
### Example
|
|
326
|
+
```tsx
|
|
327
|
+
function ProgressBar() {
|
|
328
|
+
const [progress, setProgress] = useState(0)
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (progress >= 100) return
|
|
332
|
+
const timer = setTimeout(() => {
|
|
333
|
+
setProgress(value => Math.min(value + 2, 100))
|
|
334
|
+
}, 100)
|
|
335
|
+
return () => clearTimeout(timer)
|
|
336
|
+
}, [progress])
|
|
337
|
+
|
|
338
|
+
return <div style={{ width: `${progress}%` }} />
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Usage: <ProgressBar key={currentIndex} />
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Why it works
|
|
345
|
+
The `key` remounts the component and restarts its local progress. React runs the previous Effect cleanup during that unmount; the timeout also stops once progress reaches `100`.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Function Placement Around Effects
|
|
350
|
+
|
|
351
|
+
**Evidence:** Field-validated in a real audit.
|
|
352
|
+
|
|
353
|
+
### Context
|
|
354
|
+
A function declared with `const` after an Effect is not automatically a JavaScript temporal-dead-zone error: an Effect runs after the component body finishes. However, source order can make a callback harder to inspect and can interact with Hook lint/compiler analysis when the function is recreated during render or omitted from dependencies.
|
|
355
|
+
|
|
356
|
+
*Source: [react.dev — Specifying reactive dependencies](https://react.dev/reference/react/useEffect#specifying-reactive-dependencies)*
|
|
357
|
+
|
|
358
|
+
### Safer structure
|
|
359
|
+
When a helper is used only by one Effect, define it inside that Effect:
|
|
360
|
+
```tsx
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
async function load() {
|
|
363
|
+
const response = await fetch(`/api/items/${itemId}`)
|
|
364
|
+
if (!response.ok) throw new Error('Failed to load item')
|
|
365
|
+
setData(await response.json())
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
void load()
|
|
369
|
+
}, [itemId])
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
When a helper is shared, define it before its consumers and make its reactive dependencies explicit with `useCallback`, or restructure the flow so the Effect has complete dependencies.
|
|
373
|
+
|
|
374
|
+
### Verify first
|
|
375
|
+
Read the exact lint/compiler finding before reordering code. The fix may be source order, dependency management, or moving a helper into the Effect; it is not automatically a hoisting fix.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## exhaustive-deps Suppressions
|
|
380
|
+
|
|
381
|
+
### Problem
|
|
382
|
+
`eslint-disable-next-line react-hooks/exhaustive-deps` scattered throughout the code. Each suppression hides a specific reason.
|
|
383
|
+
|
|
384
|
+
*Source: [react.dev — useEffect: Specifying reactive dependencies](https://react.dev/reference/react/useEffect#specifying-reactive-dependencies)*
|
|
385
|
+
|
|
386
|
+
### Principle
|
|
387
|
+
Dependencies are determined by the reactive values read by the Effect; they are not selected to control when it runs. If adding a required dependency causes a loop or repeated request, restructure the Effect or its object/function inputs instead of retaining a suppression.
|
|
388
|
+
|
|
389
|
+
A suppression should be rare, documented with the proven invariant, and revisited when the Effect changes.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
## useRef Without Initial Value in React 19
|
|
396
|
+
|
|
397
|
+
### Problem
|
|
398
|
+
React 19 requires `useRef` to have an explicit initial value.
|
|
399
|
+
|
|
400
|
+
### Code (symptom)
|
|
401
|
+
```ts
|
|
402
|
+
const ref = useRef<HTMLDivElement>() // ❌ error in React 19
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Fix
|
|
406
|
+
```ts
|
|
407
|
+
const ref = useRef<HTMLDivElement>(null) // ✅
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Variations
|
|
411
|
+
| Type | Fix |
|
|
412
|
+
|---|---|
|
|
413
|
+
| DOM Element | `useRef<HTMLDivElement>(null)` |
|
|
414
|
+
| Numeric value | `useRef<number>(0)` |
|
|
415
|
+
| String value | `useRef<string>('')` |
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## State Mutation
|
|
420
|
+
|
|
421
|
+
### Problem
|
|
422
|
+
Directly mutating objects or arrays in state instead of replacing them with new ones.
|
|
423
|
+
|
|
424
|
+
*Source: [react.dev — Updating Objects in State](https://react.dev/learn/updating-objects-in-state)*
|
|
425
|
+
|
|
426
|
+
### Code (symptom)
|
|
427
|
+
```tsx
|
|
428
|
+
const [user, setUser] = useState({ name: 'Alex', email: 'alex@example.com' })
|
|
429
|
+
user.name = 'Morgan' // ← direct mutation
|
|
430
|
+
setUser(user) // ← React may bail out (same reference)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Fix
|
|
434
|
+
```tsx
|
|
435
|
+
setUser({ ...user, name: 'Morgan' })
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Why it is unsafe
|
|
439
|
+
React may bail out when the reference is unchanged, but a later unrelated render can expose the mutated value. Never mutate a value held in React state directly; use a new object/array or a library such as Immer for complex nested updates.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Choose an Async UI Strategy Before Adding an Effect
|
|
444
|
+
|
|
445
|
+
### Decision
|
|
446
|
+
A request for loading UI does not automatically require `useEffect`, `Suspense`, or a new data-fetching library. First inspect the established project approach: framework data loading, query hooks, shared API client, and existing loading, error, empty, and success-state components.
|
|
447
|
+
|
|
448
|
+
### Use the existing project pattern when
|
|
449
|
+
- The project already has query hooks, route loaders, or a shared async-state abstraction for the same boundary.
|
|
450
|
+
- Existing screens expose loading, error, empty, and retry behavior consistently.
|
|
451
|
+
- The pattern is compatible with the installed framework and is not the verified cause of the issue.
|
|
452
|
+
|
|
453
|
+
### Choose deliberately
|
|
454
|
+
- Use a framework-integrated loading path when the active framework provides one for the route or server-rendered boundary.
|
|
455
|
+
- Use an existing client cache/query abstraction when caching, deduplication, invalidation, or shared server state is required.
|
|
456
|
+
- Use an Effect to synchronize a client component with an external system when no established data layer owns that work. Include cancellation or stale-response protection, loading, error, and cleanup behavior.
|
|
457
|
+
- Compute synchronous derived values during render rather than creating an async state machine for them.
|
|
458
|
+
|
|
459
|
+
Do not introduce a second fetching strategy beside an established one merely because it is also supported by React.
|
|
460
|
+
|
|
461
|
+
*Sources: [React — Fetching data with Effects](https://react.dev/reference/react/useEffect#fetching-data-with-effects) · [React — You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)*
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Suspense Boundaries Are Not Data Fetchers
|
|
466
|
+
|
|
467
|
+
### Decision
|
|
468
|
+
`Suspense` renders a fallback while a descendant suspends. It does not initiate an API request, provide cache invalidation, or replace loading/error handling by itself.
|
|
469
|
+
|
|
470
|
+
### Appropriate uses
|
|
471
|
+
- Code splitting with `lazy`.
|
|
472
|
+
- Framework or library data sources explicitly documented as Suspense-compatible.
|
|
473
|
+
- A deliberate loading boundary whose fallback and reveal behavior match the existing UI architecture.
|
|
474
|
+
|
|
475
|
+
### Verify first
|
|
476
|
+
- Confirm the installed framework or data library documents the exact Suspense integration in use.
|
|
477
|
+
- Check whether route-level loading or the existing query layer already owns the loading state.
|
|
478
|
+
- Place boundaries around a meaningful visual unit; avoid a single fallback that blocks unrelated content unless that is the intended experience.
|
|
479
|
+
- Provide an error boundary or the project’s existing error treatment for failures. A Suspense fallback does not render request errors.
|
|
480
|
+
|
|
481
|
+
Do not replace a working project loading convention with Suspense only because React supports it.
|
|
482
|
+
|
|
483
|
+
*Sources: [React — Suspense](https://react.dev/reference/react/Suspense) · [React — lazy](https://react.dev/reference/react/lazy)*
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Extract a Custom Hook Only for a Reusable Stateful Contract
|
|
488
|
+
|
|
489
|
+
### Decision
|
|
490
|
+
Extract a custom hook when multiple consumers need the same stateful behavior, lifecycle handling, or integration contract—not simply because a component contains several lines of code.
|
|
491
|
+
|
|
492
|
+
### Verify first
|
|
493
|
+
- Identify at least one current or credible near-term consumer with the same inputs, outputs, and lifecycle.
|
|
494
|
+
- Keep UI markup, labels, and component-specific presentation in the component.
|
|
495
|
+
- Preserve the project’s naming, folder, testing, and return-shape conventions for hooks.
|
|
496
|
+
- Prefer a local helper when the logic has one consumer and extraction would obscure the flow.
|
|
497
|
+
|
|
498
|
+
A custom hook should expose a cohesive behavior contract, such as a subscription lifecycle or form interaction state. It must still follow the Rules of Hooks and keep reactive dependencies explicit.
|
|
499
|
+
|
|
500
|
+
*Sources: [React — Reusing Logic with Custom Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) · [React — Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)*
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# 🎨 Tailwind CSS and shadcn/ui Patterns
|
|
2
|
+
|
|
3
|
+
> **Purpose:** Diagnose migration, scanning, token, theme, and component-installation problems.
|
|
4
|
+
> **Primary official sources:** [Tailwind v4 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide) · [Tailwind Detecting Classes](https://tailwindcss.com/docs/detecting-classes-in-source-files) · [Tailwind Theme Variables](https://tailwindcss.com/docs/theme) · [shadcn/ui Documentation](https://ui.shadcn.com/docs) · [Radix Primitives](https://www.radix-ui.com/primitives/docs/overview/introduction)
|
|
5
|
+
|
|
6
|
+
## Project Convention Decision Rule
|
|
7
|
+
|
|
8
|
+
Before applying this guidance, verify the installed versions, local rules, design tokens, component abstractions, class conventions, configuration, and tests. Preserve a valid established project convention; do not replace it only because another documented approach is also valid. Use official sources to verify API behavior, compatibility, constraints, and migrations. Recommend a change only when the developer requests it or evidence shows the current approach is incompatible, unsafe, deprecated, broken, or responsible for a verified issue.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
1. [Tailwind v3 to v4 Is a Migration Boundary](#tailwind-v3-to-v4-is-a-migration-boundary)
|
|
15
|
+
2. [Classes Are Missing from Generated CSS](#classes-are-missing-from-generated-css)
|
|
16
|
+
3. [CSS Variables and Utility Tokens Drift](#css-variables-and-utility-tokens-drift)
|
|
17
|
+
4. [shadcn/ui Component Dependencies Are Missing](#shadcnui-component-dependencies-are-missing)
|
|
18
|
+
5. [Radix Unified and Individual Imports Are Mixed](#radix-unified-and-individual-imports-are-mixed)
|
|
19
|
+
6. [Dark Mode or Theme Tokens Do Not Apply](#dark-mode-or-theme-tokens-do-not-apply)
|
|
20
|
+
7. [Extend the Existing Design System Before Adding Local Styles](#extend-the-existing-design-system-before-adding-local-styles)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Tailwind v3 to v4 Is a Migration Boundary
|
|
25
|
+
|
|
26
|
+
### Symptom
|
|
27
|
+
A version upgrade leaves utilities ungenerated, configuration ignored, or build integration broken.
|
|
28
|
+
|
|
29
|
+
### Verify first
|
|
30
|
+
Identify the installed major version and the active integration. Tailwind v3 uses a JavaScript configuration and `content` array; Tailwind v4 has a CSS-first configuration model and different installation/migration guidance. Do not apply v4 directives or package setup piecemeal to a v3 build.
|
|
31
|
+
|
|
32
|
+
### Fix
|
|
33
|
+
Follow the official upgrade guide as one migration, including the appropriate PostCSS or Vite integration, import syntax, and source-detection approach. Preserve a working v3 setup until the v4 migration is complete and validated.
|
|
34
|
+
|
|
35
|
+
*Source: [Tailwind CSS v4.0 Upgrade Guide](https://tailwindcss.com/docs/upgrade-guide)*
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Classes Are Missing from Generated CSS
|
|
40
|
+
|
|
41
|
+
### Symptom
|
|
42
|
+
A class appears in source but has no effect in the browser, especially in a component package, monorepo, or dynamically constructed class string.
|
|
43
|
+
|
|
44
|
+
### Verify first
|
|
45
|
+
Confirm the exact class is present as a complete static string in a scanned source file. Tailwind scans source as plain text; it does not infer class names assembled at runtime.
|
|
46
|
+
|
|
47
|
+
### Fix
|
|
48
|
+
For Tailwind v3, include every relevant source path in `content`. For Tailwind v4, use automatic detection where applicable and explicitly register ignored or external sources with `@source` when needed. Map runtime choices to complete class names rather than interpolating fragments.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const variants = {
|
|
52
|
+
success: 'bg-green-600 text-white',
|
|
53
|
+
danger: 'bg-red-600 text-white',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return <button className={variants[state]} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
*Sources: [Tailwind — Content Configuration (v3)](https://v3.tailwindcss.com/docs/content-configuration), [Tailwind — Detecting Classes](https://tailwindcss.com/docs/detecting-classes-in-source-files)*
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## CSS Variables and Utility Tokens Drift
|
|
64
|
+
|
|
65
|
+
### Symptom
|
|
66
|
+
A semantic utility such as `bg-primary` uses an unexpected color, or a component looks correct in one theme and incorrect in another.
|
|
67
|
+
|
|
68
|
+
### Verify first
|
|
69
|
+
Trace the value from the utility name to its CSS variable and then to the active theme selector. Confirm the variable format matches how the utility consumes it (for example, a raw color value versus channel values used with an alpha placeholder).
|
|
70
|
+
|
|
71
|
+
### Fix
|
|
72
|
+
Define one semantic token contract and use it consistently across base and alternate themes. In Tailwind v4, expose theme values with `@theme` when a utility namespace is required; retain ordinary CSS custom properties for runtime theme switching where appropriate. Avoid duplicating literal colors in component classes when a semantic token exists.
|
|
73
|
+
|
|
74
|
+
*Sources: [Tailwind — Theme Variables](https://tailwindcss.com/docs/theme), [MDN — Using CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties)*
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## shadcn/ui Component Dependencies Are Missing
|
|
79
|
+
|
|
80
|
+
### Symptom
|
|
81
|
+
A copied or added component fails to compile because an import, utility, primitive package, or shared file is absent.
|
|
82
|
+
|
|
83
|
+
### Verify first
|
|
84
|
+
Check the component documentation and generated source, then inspect `components.json` aliases and the installed package manifest. shadcn/ui components are code added to the application, not a single opaque runtime package; a component can rely on utilities, icons, Radix primitives, and other components.
|
|
85
|
+
|
|
86
|
+
### Fix
|
|
87
|
+
Use the CLI command documented for the installed shadcn/ui version to add the component and its declared dependencies. If integrating manually, copy every documented dependency deliberately and preserve imports that match the project’s aliases. Run typecheck after installation; do not remove an apparently unused primitive without tracing transitive component use.
|
|
88
|
+
|
|
89
|
+
*Sources: [shadcn/ui — CLI](https://ui.shadcn.com/docs/cli), [shadcn/ui — components.json](https://ui.shadcn.com/docs/components-json)*
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Radix Unified and Individual Imports Are Mixed
|
|
94
|
+
|
|
95
|
+
### Symptom
|
|
96
|
+
Primitive imports resolve inconsistently, dependency versions duplicate, or generated component code no longer matches installed packages.
|
|
97
|
+
|
|
98
|
+
### Verify first
|
|
99
|
+
Inspect the actual import paths in component source and the package manifest. Radix offers documented package entry points; different installation styles can have different import paths and version constraints.
|
|
100
|
+
|
|
101
|
+
### Fix
|
|
102
|
+
Choose the import style documented for the installed Radix packages and keep generated component imports consistent with it. Do not mechanically rewrite individual `@radix-ui/react-*` imports to a unified package, or the reverse, without verifying the installed API, exports, peer dependencies, and build output.
|
|
103
|
+
|
|
104
|
+
*Sources: [Radix Primitives — Introduction](https://www.radix-ui.com/primitives/docs/overview/introduction), [Radix Primitives — Releases](https://www.radix-ui.com/primitives/docs/overview/releases)*
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Dark Mode or Theme Tokens Do Not Apply
|
|
109
|
+
|
|
110
|
+
### Symptom
|
|
111
|
+
The theme toggle changes state but colors do not change, or only part of the interface switches.
|
|
112
|
+
|
|
113
|
+
### Verify first
|
|
114
|
+
In browser developer tools, verify: (1) the expected class or data attribute is applied to the intended root element, (2) the alternate-theme CSS variables match that selector, and (3) rendered elements use semantic token utilities rather than hard-coded colors.
|
|
115
|
+
|
|
116
|
+
### Fix
|
|
117
|
+
Make the theme selector strategy match the Tailwind configuration or CSS variant strategy. Define the same semantic variable names in every theme, including foreground and border tokens, and ensure portal-rendered content inherits or is covered by the selector. Test the initial theme as well as toggling to detect a flash of incorrect theme.
|
|
118
|
+
|
|
119
|
+
*Sources: [Tailwind — Dark Mode](https://tailwindcss.com/docs/dark-mode), [Tailwind — Colors](https://tailwindcss.com/docs/colors)*
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Validation Checklist
|
|
124
|
+
|
|
125
|
+
1. Confirm the Tailwind major version before changing configuration or directives.
|
|
126
|
+
2. Build CSS and inspect whether a known expected utility is emitted.
|
|
127
|
+
3. Verify token values and active theme selectors in browser developer tools.
|
|
128
|
+
4. Typecheck after adding a shadcn/ui component to catch missing dependencies and alias failures.
|
|
129
|
+
5. Test keyboard interaction and focus behavior for installed Radix-based components.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Extend the Existing Design System Before Adding Local Styles
|
|
134
|
+
|
|
135
|
+
### Decision
|
|
136
|
+
Before creating a one-off component, utility combination, color, radius, or spacing scale, inspect the design tokens, reusable primitives, variants, and nearby comparable UI. A working design system is a project convention, not an obstacle to bypass.
|
|
137
|
+
|
|
138
|
+
### Verify first
|
|
139
|
+
- Reuse an existing component or variant when it serves the same semantic role.
|
|
140
|
+
- Prefer semantic token utilities over literal colors when the project defines them.
|
|
141
|
+
- Follow the project’s existing class-composition and variant conventions.
|
|
142
|
+
- Preserve accessibility behavior, focus handling, and responsive states supplied by an existing primitive.
|
|
143
|
+
|
|
144
|
+
Create a local style only when the existing system cannot express the requirement without distortion. If a new primitive or token is needed, introduce it deliberately at the same ownership layer as comparable project primitives rather than embedding a private design system in one feature.
|
|
145
|
+
|
|
146
|
+
*Sources: [Tailwind — Theme Variables](https://tailwindcss.com/docs/theme) · [shadcn/ui — Theming](https://ui.shadcn.com/docs/theming) · [Radix Primitives — Accessibility](https://www.radix-ui.com/primitives/docs/overview/accessibility)*
|