@dynokostya/just-works 1.0.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.
- package/.claude/agents/csharp-code-writer.md +32 -0
- package/.claude/agents/diagrammer.md +49 -0
- package/.claude/agents/frontend-code-writer.md +36 -0
- package/.claude/agents/prompt-writer.md +38 -0
- package/.claude/agents/python-code-writer.md +32 -0
- package/.claude/agents/swift-code-writer.md +32 -0
- package/.claude/agents/typescript-code-writer.md +32 -0
- package/.claude/commands/git-sync.md +96 -0
- package/.claude/commands/project-docs.md +287 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.json.default +15 -0
- package/.claude/skills/csharp-coding/SKILL.md +368 -0
- package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.claude/skills/python-coding/SKILL.md +293 -0
- package/.claude/skills/react-coding/SKILL.md +264 -0
- package/.claude/skills/rest-api/SKILL.md +421 -0
- package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.claude/skills/swift-coding/SKILL.md +401 -0
- package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.claude/skills/typescript-coding/SKILL.md +464 -0
- package/.claude/statusline-command.sh +34 -0
- package/.codex/prompts/plan-reviewer.md +162 -0
- package/.codex/prompts/project-docs.md +287 -0
- package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.codex/skills/python-coding/SKILL.md +293 -0
- package/.codex/skills/react-coding/SKILL.md +264 -0
- package/.codex/skills/rest-api/SKILL.md +421 -0
- package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.codex/skills/typescript-coding/SKILL.md +464 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/bin/cli.mjs +291 -0
- package/package.json +39 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-coding
|
|
3
|
+
description: Apply when writing or editing Python (.py) files. Behavioral corrections for error handling, resource management, async patterns, data modeling, type safety, security defaults, and common antipatterns. Project conventions always override these defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Coding
|
|
7
|
+
|
|
8
|
+
Match the project's existing conventions. When uncertain, read 2-3 existing modules to infer the local style. Check `pyproject.toml` for Python version target, linter config, and tooling. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent bugs and vulnerabilities regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never `except: pass`** or bare `except Exception` without re-raise. Catch specific exception types. Broad catches silently swallow bugs — a `KeyError` from a typo looks the same as a network failure, and you'll spend hours debugging something the traceback would have told you instantly.
|
|
15
|
+
- **Never `datetime.now()` or `datetime.utcnow()`** -- both produce naive datetimes that lose timezone info. Naive datetimes cause subtle bugs when code crosses timezone boundaries (servers, users, DST). Use `datetime.now(tz=timezone.utc)`. Use `zoneinfo.ZoneInfo` for other timezones, not `pytz`.
|
|
16
|
+
- **Never `random` for security** -- `random` uses a predictable PRNG; an attacker who observes a few outputs can predict future ones. Use `secrets.token_hex()`, `secrets.token_urlsafe()`, or `secrets.token_bytes()` for tokens, keys, session IDs.
|
|
17
|
+
- **Never `shell=True` in `subprocess`** -- shell interpretation enables command injection if any argument contains user input. Use argument lists: `subprocess.run(["cmd", arg1, arg2])`.
|
|
18
|
+
- **Never string formatting in SQL** -- SQL injection is one of the most exploited vulnerabilities. Use parameterized queries only. `f"SELECT * FROM users WHERE id = {uid}"` is always a defect.
|
|
19
|
+
- **Never `yaml.load()`** without `SafeLoader` -- use `yaml.safe_load()`. Unsafe YAML loading deserializes arbitrary Python objects, enabling remote code execution from a crafted YAML file.
|
|
20
|
+
- **Never `pickle.load()` on untrusted data** -- pickle executes arbitrary code during deserialization by design. Use JSON, MessagePack, or Protocol Buffers for data interchange.
|
|
21
|
+
- **Never `eval()` or `exec()` on external input** -- use `ast.literal_eval()` for safe evaluation of literals.
|
|
22
|
+
- **Never mutable default arguments** -- `def f(items=[])` shares one list across all calls. Appending in one call mutates the default for every subsequent call. Use `None` sentinel: `def f(items: list[str] | None = None)` then `items = items or []`.
|
|
23
|
+
- **Never shadow builtins** -- don't use `list`, `dict`, `id`, `type`, `input`, `hash`, `map`, `set`, `filter` as variable names. Shadowing causes confusing errors when you later need the builtin in the same scope.
|
|
24
|
+
- **Never blocking calls in async** -- no `time.sleep()`, bare `open()`, or `requests.get()` inside `async def`. These block the entire event loop, freezing all concurrent tasks. Use `asyncio.sleep()`, `aiofiles`, `httpx`.
|
|
25
|
+
- **Never `+=` string concatenation in loops** -- use `"".join(parts)`. Python strings are immutable, so each `+=` allocates a new string and copies all previous content — O(n²) at scale. At 10k iterations this turns milliseconds into seconds.
|
|
26
|
+
|
|
27
|
+
## Error handling
|
|
28
|
+
|
|
29
|
+
Always use `raise ... from e` when re-raising at I/O boundaries. This preserves the original traceback -- essential for production debugging. Use `raise ... from None` only when the original exception is genuinely irrelevant to the caller.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
async def get_user(user_id: int) -> User:
|
|
33
|
+
try:
|
|
34
|
+
result = await db.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
|
|
35
|
+
except asyncpg.PostgresError as e:
|
|
36
|
+
raise DatabaseError(f"Failed to query user {user_id}") from e
|
|
37
|
+
if result is None:
|
|
38
|
+
raise UserNotFoundError(user_id)
|
|
39
|
+
return User(**result)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create custom exception types when callers need to distinguish failure modes:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
class AppError(Exception):
|
|
46
|
+
"""Base exception."""
|
|
47
|
+
|
|
48
|
+
class NotFoundError(AppError):
|
|
49
|
+
def __init__(self, resource: str, id: Any) -> None:
|
|
50
|
+
self.resource = resource
|
|
51
|
+
self.id = id
|
|
52
|
+
super().__init__(f"{resource} not found: {id}")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When logging a caught exception, always preserve the traceback:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Wrong: traceback lost
|
|
59
|
+
except httpx.HTTPStatusError as e:
|
|
60
|
+
logger.error(f"API call failed: {e}")
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
# Correct: logger.exception() includes full traceback
|
|
64
|
+
except httpx.HTTPStatusError:
|
|
65
|
+
logger.exception("API call failed")
|
|
66
|
+
raise
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use `exc_info=True` for non-error log levels: `logger.warning("retrying", exc_info=True)`.
|
|
70
|
+
|
|
71
|
+
## Resource cleanup
|
|
72
|
+
|
|
73
|
+
Use context managers for anything that needs cleanup -- clients, connections, file handles. Never instantiate `httpx.AsyncClient()`, database pools, or similar without a context manager or explicit `finally` cleanup.
|
|
74
|
+
|
|
75
|
+
For managing multiple async resources, use `AsyncExitStack`:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from contextlib import AsyncExitStack
|
|
79
|
+
|
|
80
|
+
async def setup_resources() -> AsyncIterator[Resources]:
|
|
81
|
+
async with AsyncExitStack() as stack:
|
|
82
|
+
db = await stack.enter_async_context(create_pool(dsn))
|
|
83
|
+
cache = await stack.enter_async_context(create_redis(url))
|
|
84
|
+
yield Resources(db=db, cache=cache)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For custom resource lifecycles where no built-in context manager exists:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from contextlib import asynccontextmanager
|
|
91
|
+
|
|
92
|
+
@asynccontextmanager
|
|
93
|
+
async def managed_session(config: Config) -> AsyncIterator[Session]:
|
|
94
|
+
session = await Session.connect(config)
|
|
95
|
+
try:
|
|
96
|
+
yield session
|
|
97
|
+
finally:
|
|
98
|
+
await session.disconnect()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Async patterns
|
|
102
|
+
|
|
103
|
+
**Prefer `TaskGroup` (3.11+) over `gather` for most concurrent work.** TaskGroup enforces structured concurrency: if one task fails, siblings are cancelled and errors are raised as `ExceptionGroup`. `gather(return_exceptions=True)` silently mixes exceptions into results, which is error-prone. Use `gather` when you genuinely need partial results despite failures, or when targeting Python < 3.11.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
async with asyncio.TaskGroup() as tg:
|
|
107
|
+
task1 = tg.create_task(fetch_users())
|
|
108
|
+
task2 = tg.create_task(fetch_orders())
|
|
109
|
+
# Both guaranteed complete here. If either failed, ExceptionGroup is raised.
|
|
110
|
+
|
|
111
|
+
# Handle multiple failure types with except*
|
|
112
|
+
try:
|
|
113
|
+
async with asyncio.TaskGroup() as tg:
|
|
114
|
+
tg.create_task(operation_a())
|
|
115
|
+
tg.create_task(operation_b())
|
|
116
|
+
except* ConnectionError as eg:
|
|
117
|
+
for exc in eg.exceptions:
|
|
118
|
+
logger.error(f"Connection failed: {exc}")
|
|
119
|
+
except* ValueError as eg:
|
|
120
|
+
for exc in eg.exceptions:
|
|
121
|
+
logger.error(f"Validation failed: {exc}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Share a single `AsyncClient` across concurrent requests -- don't create one per call. Configure timeouts explicitly:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
async with httpx.AsyncClient(base_url="https://api.example.com", timeout=30.0) as client:
|
|
128
|
+
async with asyncio.TaskGroup() as tg:
|
|
129
|
+
task1 = tg.create_task(client.get("/users"))
|
|
130
|
+
task2 = tg.create_task(client.get("/orders"))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Type hints
|
|
134
|
+
|
|
135
|
+
Use modern syntax: `list[str]`, `dict[str, Any]`, `X | None`. Use native type parameter syntax when the project targets 3.12+; fall back to `TypeVar` for older targets.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
class Repository[T]:
|
|
139
|
+
def __init__(self, model_class: type[T]) -> None:
|
|
140
|
+
self._model_class = model_class
|
|
141
|
+
self._items: dict[int, T] = {}
|
|
142
|
+
|
|
143
|
+
def get(self, id: int) -> T | None:
|
|
144
|
+
return self._items.get(id)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Use `object` instead of `Any`** when you mean "accepts anything." `Any` silently disables type checking -- it's a hole in type safety. Reserve `Any` for when the type system genuinely cannot express something.
|
|
148
|
+
|
|
149
|
+
**Covariant inputs, concrete outputs.** Function parameters should accept abstract types (`Sequence`, `Mapping`, `Iterable`); return types should be concrete (`list`, `dict`):
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from collections.abc import Sequence, Mapping
|
|
153
|
+
|
|
154
|
+
def process_items(items: Sequence[str]) -> list[str]: # accepts tuple, list, etc.
|
|
155
|
+
return [item.upper() for item in items]
|
|
156
|
+
|
|
157
|
+
def merge_configs(base: Mapping[str, Any], override: Mapping[str, Any]) -> dict[str, Any]:
|
|
158
|
+
return {**base, **override}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Use `TYPE_CHECKING` for import-only types.** When a type is only needed for annotations (not at runtime), import it under `TYPE_CHECKING` to avoid circular imports and reduce startup cost:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from __future__ import annotations
|
|
165
|
+
from typing import TYPE_CHECKING
|
|
166
|
+
|
|
167
|
+
if TYPE_CHECKING:
|
|
168
|
+
from myapp.services import PaymentService
|
|
169
|
+
|
|
170
|
+
class OrderProcessor:
|
|
171
|
+
def __init__(self, payments: PaymentService) -> None:
|
|
172
|
+
self._payments = payments
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Pattern matching
|
|
176
|
+
|
|
177
|
+
Use `match`/`case` (3.10+) when branching on structure — it's clearer than if/elif chains for destructuring dicts, tuples, or typed objects. Don't use it as a substitute for simple value comparisons where `if/elif` reads fine.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
match event:
|
|
181
|
+
case {"type": "click", "target": target}:
|
|
182
|
+
handle_click(target)
|
|
183
|
+
case {"type": "scroll", "offset": int(offset)} if offset > 0:
|
|
184
|
+
handle_scroll(offset)
|
|
185
|
+
case _:
|
|
186
|
+
logger.warning("Unhandled event: %s", event.get("type"))
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Data modeling
|
|
190
|
+
|
|
191
|
+
| Use Case | Choice | Reason |
|
|
192
|
+
|----------|--------|--------|
|
|
193
|
+
| API request/response | Pydantic | Validation, serialization, OpenAPI |
|
|
194
|
+
| Config from env/files | Pydantic | Built-in settings management |
|
|
195
|
+
| Internal data transfer | dataclass | Lighter weight, no runtime validation |
|
|
196
|
+
| Simple value objects | dataclass | Minimal boilerplate |
|
|
197
|
+
|
|
198
|
+
Pydantic at system boundaries:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
class UserCreate(BaseModel):
|
|
202
|
+
email: str = Field(..., min_length=5)
|
|
203
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
204
|
+
|
|
205
|
+
class UserResponse(BaseModel):
|
|
206
|
+
id: int
|
|
207
|
+
email: str
|
|
208
|
+
model_config = {"from_attributes": True}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Dataclasses internally. Use `frozen=True` for value objects, `slots=True` when you have many instances:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class CacheKey:
|
|
216
|
+
namespace: str
|
|
217
|
+
id: str
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Dependency injection
|
|
221
|
+
|
|
222
|
+
Constructor injection. Dependencies are explicit, testable, and swappable.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
class UserService:
|
|
226
|
+
def __init__(self, repository: UserRepository, email_client: EmailClient) -> None:
|
|
227
|
+
self._repository = repository
|
|
228
|
+
self._email_client = email_client
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Protocol
|
|
232
|
+
|
|
233
|
+
Use Protocol for structural interfaces -- duck typing with full type safety, no inheritance required.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from typing import Protocol, runtime_checkable
|
|
237
|
+
|
|
238
|
+
@runtime_checkable
|
|
239
|
+
class Serializable(Protocol):
|
|
240
|
+
def to_dict(self) -> dict[str, Any]: ...
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Enums
|
|
244
|
+
|
|
245
|
+
Use `StrEnum` instead of raw string literals for known value sets. Catches typos at type-check time. Use `IntEnum` only when interfacing with systems that require integer codes.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from enum import StrEnum, unique
|
|
249
|
+
|
|
250
|
+
@unique
|
|
251
|
+
class OrderStatus(StrEnum):
|
|
252
|
+
PENDING = "pending"
|
|
253
|
+
CONFIRMED = "confirmed"
|
|
254
|
+
SHIPPED = "shipped"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Imports
|
|
258
|
+
|
|
259
|
+
Import order: standard library, third-party, local. Define `__all__` if the project convention uses it. Use `pathlib.Path` over `os.path`. No import-time side effects -- don't connect to databases, read files, or run computations at module level. Defer to first use.
|
|
260
|
+
|
|
261
|
+
## Retry logic
|
|
262
|
+
|
|
263
|
+
Use tenacity for transient errors: network failures, rate limits, 5xx responses. Let 4xx errors propagate -- they indicate a request problem, not a transient failure.
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
@retry(
|
|
267
|
+
retry=retry_if_exception_type((httpx.RequestError, httpx.HTTPStatusError)),
|
|
268
|
+
stop=stop_after_attempt(3),
|
|
269
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
270
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
271
|
+
reraise=True,
|
|
272
|
+
)
|
|
273
|
+
async def fetch_with_retry(url: str) -> dict[str, Any]:
|
|
274
|
+
async with httpx.AsyncClient() as client:
|
|
275
|
+
response = await client.get(url, timeout=30.0)
|
|
276
|
+
response.raise_for_status()
|
|
277
|
+
return response.json()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Logging
|
|
281
|
+
|
|
282
|
+
Match the project's logging library (stdlib `logging`, `structlog`, etc.). With stdlib, use `__name__` and pass structured data via `extra`:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
logger = logging.getLogger(__name__)
|
|
286
|
+
logger.info("User created", extra={"user_id": user.id})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Testing
|
|
290
|
+
|
|
291
|
+
Match the project's test runner and async plugin. Check for `pytest-asyncio` `mode = "auto"` (no manual `@pytest.mark.asyncio` needed) or `anyio`-based setup.
|
|
292
|
+
|
|
293
|
+
When to mock: external APIs with rate limits/costs, error paths, deterministic results. When to use real services: verifying actual integration behavior, sandbox/test modes available. Test behavior, not implementation -- test what a function returns, not how it does it internally.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-coding
|
|
3
|
+
description: Apply when writing or editing React (.tsx/.jsx) files. Behavioral corrections for hooks, state management, component structure, memoization, security, and common antipatterns. Project conventions always override these defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Coding
|
|
7
|
+
|
|
8
|
+
Match the project's existing conventions. When uncertain, read 2-3 existing components to infer the local style. Check `tsconfig.json` for strictness settings, `package.json` for React version, and framework config (Next.js, Remix, Vite) for routing and rendering conventions. These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent bugs, wasted renders, and vulnerabilities regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never `useEffect` for derived state** -- if a value can be computed from props or state, compute it during render. `useEffect` + `setState` for derivable values causes an unnecessary extra render cycle. This is the most common React mistake.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// Wrong: extra render cycle for a value computable from props
|
|
18
|
+
const [fullName, setFullName] = useState("");
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setFullName(`${first} ${last}`);
|
|
21
|
+
}, [first, last]);
|
|
22
|
+
|
|
23
|
+
// Correct: compute during render
|
|
24
|
+
const fullName = `${first} ${last}`;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **Never `useEffect` for event-driven logic** -- user-triggered actions belong in event handlers, not effects keyed to state flags. Effects are for synchronizing with external systems.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Wrong: effect triggered by state flag
|
|
31
|
+
const [submitted, setSubmitted] = useState(false);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (submitted) sendAnalytics();
|
|
34
|
+
}, [submitted]);
|
|
35
|
+
|
|
36
|
+
// Correct: logic in the event handler
|
|
37
|
+
function handleSubmit() {
|
|
38
|
+
sendAnalytics();
|
|
39
|
+
navigate("/done");
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **Never fetch data in `useEffect` without cleanup** -- missing `AbortController` causes race conditions on rapid prop changes. Prefer TanStack Query or framework data fetching over raw `useEffect` fetching entirely.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Wrong: no abort, race condition on rapid userId changes
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
|
|
49
|
+
}, [userId]);
|
|
50
|
+
|
|
51
|
+
// Correct: AbortController for cleanup
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
fetch(`/api/users/${userId}`, { signal: controller.signal })
|
|
55
|
+
.then(r => r.json())
|
|
56
|
+
.then(setUser)
|
|
57
|
+
.catch(e => { if (e.name !== "AbortError") throw e; });
|
|
58
|
+
return () => controller.abort();
|
|
59
|
+
}, [userId]);
|
|
60
|
+
|
|
61
|
+
// Best: TanStack Query handles caching, deduplication, and cancellation
|
|
62
|
+
const { data: user } = useQuery({
|
|
63
|
+
queryKey: ["user", userId],
|
|
64
|
+
queryFn: () => fetchUser(userId),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Never chain `useEffect` calls that sync state to other state** -- cascading effects create render waterfalls (render, effect, setState, render, effect, setState…). Each link in the chain adds a full render cycle, so three chained effects means four renders for one user action — visible jank and wasted CPU. Compute derived values inline or batch updates in event handlers.
|
|
69
|
+
|
|
70
|
+
- **Never mutate state directly** -- React detects changes by reference. `array.push()` and `obj.prop = x` won't trigger re-renders. Use spread, `structuredClone`, or non-mutating methods like `.toSorted()` (not `.sort()` which mutates).
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// Wrong: mutates existing array, React sees same reference
|
|
74
|
+
items.push(newItem);
|
|
75
|
+
setItems(items);
|
|
76
|
+
|
|
77
|
+
// Correct: new array reference, non-mutating sort
|
|
78
|
+
setItems([...items, newItem].toSorted((a, b) => a.name.localeCompare(b.name)));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **Never use array index as `key` for dynamic lists** -- index keys cause React to reuse DOM nodes incorrectly when items are reordered, inserted, or removed, corrupting component state. Use stable unique IDs.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
// Wrong: index key breaks on reorder/insert/delete
|
|
85
|
+
{items.map((item, i) => <ListItem key={i} item={item} />)}
|
|
86
|
+
|
|
87
|
+
// Correct: stable unique ID
|
|
88
|
+
{items.map(item => <ListItem key={item.id} item={item} />)}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **Never add `"use client"` by default** -- in Next.js App Router, components are Server Components by default. Adding `"use client"` unnecessarily pushes components and their entire subtree to the client bundle, losing server-side rendering, data fetching, and streaming benefits. Only add it when the component uses hooks (`useState`, `useEffect`), event handlers, or browser APIs. Place the boundary as low in the tree as possible.
|
|
92
|
+
|
|
93
|
+
- **Never use `forwardRef` in React 19+** -- `ref` is a regular prop in React 19+. `forwardRef` is deprecated. In React 18, `forwardRef` is still required.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// Wrong (React 19): forwardRef is deprecated
|
|
97
|
+
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
|
|
98
|
+
<input ref={ref} {...props} />
|
|
99
|
+
));
|
|
100
|
+
|
|
101
|
+
// Correct (React 19): ref is a regular prop
|
|
102
|
+
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
|
|
103
|
+
return <input ref={ref} {...props} />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// React 18: forwardRef is still required
|
|
107
|
+
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
|
|
108
|
+
<input ref={ref} {...props} />
|
|
109
|
+
));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- **Never define components inside other components** -- the inner component is recreated every render, destroying all state and DOM on each cycle.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// Wrong: Child is a new component identity every render
|
|
116
|
+
function Parent() {
|
|
117
|
+
function Child() { return <div>child</div>; }
|
|
118
|
+
return <Child />;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Correct: Child defined outside
|
|
122
|
+
function Child() { return <div>child</div>; }
|
|
123
|
+
function Parent() { return <Child />; }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **Never suppress `react-hooks/exhaustive-deps`** -- `eslint-disable` for this rule masks stale closures, where an effect captures an old value of a prop or state and silently operates on outdated data. The resulting bugs are intermittent and hard to trace because the component appears to work until a specific re-render order exposes the stale value. Fix the code: extract functions, use updater functions for state, or move objects inside the effect.
|
|
127
|
+
|
|
128
|
+
- **Never mirror props in state** -- `useState(prop)` captures the initial value only. Subsequent prop changes are silently ignored. Use the prop directly, or name it `initialX` to signal intent.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
// Wrong: color prop changes are silently ignored after mount
|
|
132
|
+
function Badge({ color }: { color: string }) {
|
|
133
|
+
const [badgeColor, setBadgeColor] = useState(color);
|
|
134
|
+
return <span style={{ color: badgeColor }}>badge</span>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Correct: use the prop directly
|
|
138
|
+
function Badge({ color }: { color: string }) {
|
|
139
|
+
return <span style={{ color }}>badge</span>;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **Never use `useEffect` to reset state on prop change** -- use the `key` prop to unmount/remount the component, which resets all state automatically.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Wrong: effect to reset state when userId changes
|
|
147
|
+
function Profile({ userId }: { userId: string }) {
|
|
148
|
+
const [comment, setComment] = useState("");
|
|
149
|
+
useEffect(() => { setComment(""); }, [userId]);
|
|
150
|
+
return <textarea value={comment} onChange={e => setComment(e.target.value)} />;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Correct: key forces remount, all state resets
|
|
154
|
+
<Profile key={userId} userId={userId} />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **Never generate class components** -- all modern React features (Server Components, Suspense, hooks, React Compiler) require function components. The only exception: error boundaries (which still require class components or `react-error-boundary`).
|
|
158
|
+
|
|
159
|
+
- **Never use `dangerouslySetInnerHTML` without sanitization** -- renders raw HTML, enabling XSS attacks. Always sanitize with DOMPurify or similar. Prefer React's built-in escaping over raw HTML injection entirely.
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
// Wrong: unsanitized HTML injection
|
|
163
|
+
<div dangerouslySetInnerHTML={{ __html: userContent }} />
|
|
164
|
+
|
|
165
|
+
// Correct: sanitize first
|
|
166
|
+
import DOMPurify from "dompurify";
|
|
167
|
+
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- **Never use `React.lazy` without a `<Suspense>` boundary** -- lazy components suspend during loading. Without `<Suspense>`, the thrown promise is unhandled and crashes the app. Wrap with `<Suspense>` and an error boundary.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// Wrong: no Suspense boundary, crashes on load
|
|
174
|
+
const Dashboard = lazy(() => import("./Dashboard"));
|
|
175
|
+
function App() { return <Dashboard />; }
|
|
176
|
+
|
|
177
|
+
// Correct: Suspense with fallback and error boundary
|
|
178
|
+
const Dashboard = lazy(() => import("./Dashboard"));
|
|
179
|
+
function App() {
|
|
180
|
+
return (
|
|
181
|
+
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
|
|
182
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
183
|
+
<Dashboard />
|
|
184
|
+
</Suspense>
|
|
185
|
+
</ErrorBoundary>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Memoization
|
|
191
|
+
|
|
192
|
+
**With React Compiler (v19.x + compiler enabled):** The compiler auto-memoizes components, hooks, and JSX elements. Remove manual `useMemo`, `useCallback`, and `React.memo` -- they add noise and the compiler may deopt on components where it cannot preserve manual memoization. Let the compiler handle it.
|
|
193
|
+
|
|
194
|
+
**Without React Compiler:** Memoize at measured bottlenecks only. Don't wrap everything in `useMemo`/`useCallback`/`React.memo` preemptively. When you do memoize, avoid creating new object/array literals in JSX props to memoized children -- `style={{ color: "red" }}` creates a new reference every render, defeating `React.memo`. Hoist static objects outside the component.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Wrong: new style object every render defeats React.memo on Child
|
|
198
|
+
function Parent() {
|
|
199
|
+
return <MemoizedChild style={{ color: "red" }} />;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Correct: hoist static objects outside the component
|
|
203
|
+
const childStyle = { color: "red" } as const;
|
|
204
|
+
function Parent() {
|
|
205
|
+
return <MemoizedChild style={childStyle} />;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Hooks
|
|
210
|
+
|
|
211
|
+
Rules of hooks -- no exceptions:
|
|
212
|
+
- Call hooks at the top level of function components and custom hooks only.
|
|
213
|
+
- Never call hooks inside conditions, loops, nested functions, `try`/`catch`, or after early returns.
|
|
214
|
+
- The `use()` API (React 19) is the exception -- it can be called conditionally.
|
|
215
|
+
|
|
216
|
+
**`useEffect` decision tree -- ask before writing any effect:**
|
|
217
|
+
|
|
218
|
+
1. Can the value be computed from existing props/state? Compute during render.
|
|
219
|
+
2. Is this responding to a user event? Put it in the event handler.
|
|
220
|
+
3. Do you need to reset state when a prop changes? Use `key` on the component.
|
|
221
|
+
4. Do you need to sync with an external system (DOM, network, third-party widget)? This is a valid `useEffect`. Add cleanup.
|
|
222
|
+
5. Do you need to fetch data? Prefer framework data fetching or TanStack Query. If raw `useEffect`, always use `AbortController`.
|
|
223
|
+
|
|
224
|
+
## Component typing
|
|
225
|
+
|
|
226
|
+
**Prefer typed function declarations over `React.FC`.** `React.FC` is no longer broken (implicit `children` was removed in React 18 types), but plain function declarations are clearer, support generics naturally, and are the community standard.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
interface UserCardProps {
|
|
230
|
+
user: User;
|
|
231
|
+
onSelect: (id: string) => void;
|
|
232
|
+
variant?: "compact" | "full";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function UserCard({ user, onSelect, variant = "full" }: UserCardProps) {
|
|
236
|
+
return ( /* JSX */ );
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Generic components:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
function List<T>({ items, renderItem, keyExtractor }: {
|
|
244
|
+
items: T[];
|
|
245
|
+
renderItem: (item: T) => React.ReactNode;
|
|
246
|
+
keyExtractor: (item: T) => string;
|
|
247
|
+
}) {
|
|
248
|
+
return (
|
|
249
|
+
<ul>
|
|
250
|
+
{items.map(item => (
|
|
251
|
+
<li key={keyExtractor(item)}>{renderItem(item)}</li>
|
|
252
|
+
))}
|
|
253
|
+
</ul>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Use discriminated unions for impossible states:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
type ButtonProps =
|
|
262
|
+
| { variant: "link"; href: string; onClick?: never }
|
|
263
|
+
| { variant: "button"; onClick: () => void; href?: never };
|
|
264
|
+
```
|