@3lineas/grooveskills 1.0.2 → 1.0.3
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/README.md +3 -0
- package/dist/{chunk-TYA2EELM.js → chunk-6FZFZCBX.js} +22 -1
- package/dist/chunk-6FZFZCBX.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/react-advanced/SKILL.md +147 -0
- package/skills/react-advanced/references/component-patterns.md +117 -0
- package/skills/react-advanced/references/concurrent-rendering.md +130 -0
- package/skills/react-advanced/references/performance.md +99 -0
- package/skills/react-advanced/references/react-compiler.md +90 -0
- package/skills/react-advanced/references/react19-actions-and-apis.md +145 -0
- package/skills/react-advanced/references/rsc-boundary.md +129 -0
- package/skills/react-core/SKILL.md +168 -0
- package/skills/react-create-page/SKILL.md +187 -0
- package/dist/chunk-TYA2EELM.js.map +0 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# React Compiler — Adoption & Memoization Guidance
|
|
2
|
+
|
|
3
|
+
> Source of truth: [React Compiler docs](https://react.dev/learn/react-compiler/introduction) and the [1.0 announcement](https://react.dev/blog/2025/10/07/react-compiler-1).
|
|
4
|
+
|
|
5
|
+
## What the Compiler Is
|
|
6
|
+
|
|
7
|
+
The React Compiler is a build-time optimizing compiler that rewrites components and hooks to memoize automatically. It reached **1.0 stable on 2025-10-07**. It analyzes which values a component computes, which of those depend on which inputs, and emits memoization at fine granularity — including **conditional** memoization that a hand-written `useMemo` cannot express.
|
|
8
|
+
|
|
9
|
+
It supports React 17, 18, and 19, and produces the best results on 19. On 17/18 it relies on a runtime helper (`react-compiler-runtime`).
|
|
10
|
+
|
|
11
|
+
## The Memoization Inversion
|
|
12
|
+
|
|
13
|
+
Pre-compiler React guidance optimized for reference stability by hand: wrap derived values in `useMemo`, wrap callbacks in `useCallback`, wrap components in `React.memo`. With the compiler enabled, that work is redundant for new code, and over-memoization carries real cost (larger code, more cache slots, harder reading).
|
|
14
|
+
|
|
15
|
+
### Before (manual, pre-compiler)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
export function Dashboard({ items, onSelect }: DashboardProps) {
|
|
19
|
+
const sorted = useMemo(() => items.slice().sort(byName), [items]);
|
|
20
|
+
const total = useMemo(() => items.reduce((s, i) => s + i.value, 0), [items]);
|
|
21
|
+
const handleSelect = useCallback((id: string): void => onSelect(id), [onSelect]);
|
|
22
|
+
|
|
23
|
+
return <ItemList items={sorted} total={total} onSelect={handleSelect} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ItemList = React.memo(function ItemList({ items, total, onSelect }: ListProps) {
|
|
27
|
+
return /* ... */;
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### After (compiler enabled)
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Plain code. The compiler memoizes `sorted`, `total`, `handleSelect`,
|
|
35
|
+
// and the ItemList render output as needed — including conditionally.
|
|
36
|
+
export function Dashboard({ items, onSelect }: DashboardProps) {
|
|
37
|
+
const sorted = items.slice().sort(byName);
|
|
38
|
+
const total = items.reduce((s, i) => s + i.value, 0);
|
|
39
|
+
const handleSelect = (id: string): void => onSelect(id);
|
|
40
|
+
|
|
41
|
+
return <ItemList items={sorted} total={total} onSelect={handleSelect} />;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## When Manual Memo Still Earns Its Place
|
|
46
|
+
|
|
47
|
+
Keep manual memo when:
|
|
48
|
+
|
|
49
|
+
1. **A value is an effect dependency and correctness depends on its reference identity.** The compiler optimizes *render*; it does not guarantee a particular identity contract for an effect's dependency array. A value recreated each render re-fires the effect.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
const config = useMemo(() => ({ url, token }), [url, token]);
|
|
53
|
+
useEffect(() => subscribe(config), [config]);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
2. **Code the compiler cannot statically analyze.** When the compiler cannot prove a component follows the Rules of React, it safely skips that component rather than risk a miscompile. In a skipped component, manual memo remains a valid local optimization.
|
|
57
|
+
|
|
58
|
+
## Do Not Strip Existing Manual Memo Blindly
|
|
59
|
+
|
|
60
|
+
The compiler is designed to *preserve* existing `useMemo`/`useCallback`/`React.memo`. The [`preserve-manual-memoization`](https://react.dev/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization) lint flags cases where removing or altering manual memo would change behavior. Treat its warnings as a signal that the memo carried meaning beyond performance.
|
|
61
|
+
|
|
62
|
+
## Adoption Path
|
|
63
|
+
|
|
64
|
+
1. **Install ESLint support:** `eslint-plugin-react-hooks` v6+ absorbed the former `eslint-plugin-react-compiler`.
|
|
65
|
+
2. **Fix reported Rules of React violations** (mutations during render, conditional hooks).
|
|
66
|
+
3. **Enable the compiler in the build** (Babel, Vite, Metro, Next.js, or Rsbuild).
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// vite.config.ts
|
|
70
|
+
import react from "@vitejs/plugin-react";
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
plugins: [
|
|
74
|
+
react({
|
|
75
|
+
babel: { plugins: [["babel-plugin-react-compiler", {}]] },
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
4. **Verify in DevTools:** React DevTools marks compiler-optimized components with a `"Memo ✨"` badge.
|
|
82
|
+
|
|
83
|
+
## Detection Checklist
|
|
84
|
+
|
|
85
|
+
| Symptom | Likely cause | Action |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| New code still littered with `useMemo`/`useCallback` | Reflex memoization habit | Remove; let the compiler handle it |
|
|
88
|
+
| `preserve-manual-memoization` warnings after cleanup | Removed load-bearing memo | Restore the memo; it stabilized an effect/identity contract |
|
|
89
|
+
| A component shows no `"Memo ✨"` badge | Compiler skipped it (unanalyzable) | Fix Rules of React violations, or keep manual memo there |
|
|
90
|
+
| Effect re-fires every render | A dependency value is recreated each render | Memoize that single dependency by hand |
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# React 19 Platform Hooks & APIs
|
|
2
|
+
|
|
3
|
+
> Source of truth: [use()](https://react.dev/reference/react/use), [useActionState](https://react.dev/reference/react/useActionState), [useOptimistic](https://react.dev/reference/react/useOptimistic), [useFormStatus](https://react.dev/reference/react-dom/hooks/useFormStatus), and [<form>](https://react.dev/reference/react-dom/components/form).
|
|
4
|
+
|
|
5
|
+
## The `use()` Hook
|
|
6
|
+
|
|
7
|
+
`use()` reads the value of a promise or a context. Unlike every other hook, it may be called **conditionally** — after early returns or inside branches. When given a promise, it suspends the component until the promise resolves, integrating with the nearest Suspense boundary.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { use } from "react";
|
|
11
|
+
import type { CommentItem } from "@/types/comment.types";
|
|
12
|
+
|
|
13
|
+
export function CommentList({
|
|
14
|
+
commentsPromise,
|
|
15
|
+
isVisible,
|
|
16
|
+
}: {
|
|
17
|
+
commentsPromise: Promise<CommentItem[]>;
|
|
18
|
+
isVisible: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
if (!isVisible) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Legal after an early return — `use` is exempt from the top-level rule.
|
|
25
|
+
const comments = use(commentsPromise);
|
|
26
|
+
return (
|
|
27
|
+
<ul>
|
|
28
|
+
{comments.map((c) => (
|
|
29
|
+
<li key={c.id}>{c.text}</li>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Rules and pitfalls
|
|
37
|
+
|
|
38
|
+
- **Never wrap `use()` in `try / catch`.** A suspended promise is not a synchronous error; rejections surface through an **Error Boundary**, not `catch`.
|
|
39
|
+
- **Do not create the promise during render on the client.** A promise created in the render body is recreated every render, causing infinite suspension loops. Create it on the server, in a parent loader, or in an event handler, and pass it down.
|
|
40
|
+
|
|
41
|
+
## Actions (`useActionState`, `useOptimistic`, `useFormStatus`)
|
|
42
|
+
|
|
43
|
+
Actions are React's built-in async-mutation model, providing pending, error, and optimistic handling at the React level.
|
|
44
|
+
|
|
45
|
+
### `useActionState`
|
|
46
|
+
|
|
47
|
+
Runs an action function, tracks `isPending`, and threads the previous result forward (replaces the canary `useFormState`).
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useActionState } from "react";
|
|
51
|
+
|
|
52
|
+
interface SaveResult {
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function UpdateNameForm({
|
|
57
|
+
saveName,
|
|
58
|
+
}: {
|
|
59
|
+
saveName: (name: string) => Promise<SaveResult>;
|
|
60
|
+
}) {
|
|
61
|
+
const [state, formAction, isPending] = useActionState<SaveResult, FormData>(
|
|
62
|
+
async (_prev, formData) => {
|
|
63
|
+
const name = String(formData.get("name") ?? "");
|
|
64
|
+
return await saveName(name);
|
|
65
|
+
},
|
|
66
|
+
{}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<form action={formAction}>
|
|
71
|
+
<input name="name" disabled={isPending} />
|
|
72
|
+
<button type="submit" disabled={isPending}>
|
|
73
|
+
{isPending ? "Saving..." : "Save"}
|
|
74
|
+
</button>
|
|
75
|
+
{state.error && <p role="alert">{state.error}</p>}
|
|
76
|
+
</form>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `useOptimistic`
|
|
82
|
+
|
|
83
|
+
Renders an optimistic value while an async action is in flight, reverting automatically when the action settles.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useOptimistic } from "react";
|
|
87
|
+
|
|
88
|
+
export function LikeButton({
|
|
89
|
+
count,
|
|
90
|
+
onLike,
|
|
91
|
+
}: {
|
|
92
|
+
count: number;
|
|
93
|
+
onLike: () => Promise<void>;
|
|
94
|
+
}) {
|
|
95
|
+
const [optimisticCount, addOptimistic] = useOptimistic(
|
|
96
|
+
count,
|
|
97
|
+
(current, delta: number) => current + delta
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
async function handleAction(): Promise<void> {
|
|
101
|
+
addOptimistic(1);
|
|
102
|
+
await onLike();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<form action={handleAction}>
|
|
107
|
+
<button type="submit">♥ {optimisticCount}</button>
|
|
108
|
+
</form>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `useFormStatus`
|
|
114
|
+
|
|
115
|
+
Reads the pending state of the enclosing `<form>` from a child component without prop drilling.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { useFormStatus } from "react-dom";
|
|
119
|
+
|
|
120
|
+
export function SubmitButton({ label }: { label: string }) {
|
|
121
|
+
const { pending } = useFormStatus();
|
|
122
|
+
return (
|
|
123
|
+
<button type="submit" disabled={pending}>
|
|
124
|
+
{pending ? "Submitting..." : label}
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## React 19 Ergonomic Changes
|
|
131
|
+
|
|
132
|
+
1. **`ref` as a plain prop**: `forwardRef` is deprecated. Derive wrapper props from `ComponentProps<"input">` (which already includes `ref`), or add `ref?: React.Ref<HTMLElement>` to custom interfaces.
|
|
133
|
+
2. **`<Context value>` as provider**: Render `<ThemeContext value={theme}>{children}</ThemeContext>` instead of `<ThemeContext.Provider>`.
|
|
134
|
+
3. **Native document metadata**: Render `<title>`, `<meta>`, and `<link>` directly inside components; React hoists them to `<head>` automatically without `react-helmet`.
|
|
135
|
+
|
|
136
|
+
## Migration Notes from React 17/18
|
|
137
|
+
|
|
138
|
+
| React 17/18 idiom | React 19 replacement |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `forwardRef((props, ref) => ...)` | `ref` as a plain prop |
|
|
141
|
+
| `<Context.Provider value={...}>` | `<Context value={...}>` |
|
|
142
|
+
| `react-helmet` for `<head>` tags | Native `<title>`, `<meta>`, `<link>` hoisting |
|
|
143
|
+
| `useFormState` (canary) | `useActionState` (from `"react"`) |
|
|
144
|
+
| Manual `isLoading`/`error` state in submit handlers | `useActionState` + `useOptimistic` + `useFormStatus` |
|
|
145
|
+
| Fetch in `useEffect` + store in `useState` | Pass a promise to `use()` under `<Suspense>` |
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# RSC / `'use client'` Boundary & `'use server'` Security
|
|
2
|
+
|
|
3
|
+
> Source of truth: [Server Components](https://react.dev/reference/rsc/server-components), [`'use client'`](https://react.dev/reference/rsc/use-client), and [`'use server'`](https://react.dev/reference/rsc/use-server).
|
|
4
|
+
|
|
5
|
+
## Server Components by Default
|
|
6
|
+
|
|
7
|
+
In an RSC architecture, components render on the server ahead of time and emit a serializable UI tree:
|
|
8
|
+
|
|
9
|
+
- **No client state or effects**: Server Components cannot call `useState`, `useEffect`, or browser DOM APIs.
|
|
10
|
+
- **Direct `async / await`**: Server Components can `await` database/action calls directly without shipping library code to the browser.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { getArticleByIdAction } from "@/actions/articles/get-article-by-id.action";
|
|
14
|
+
import { LikeButton } from "@/components/articles/like-button";
|
|
15
|
+
|
|
16
|
+
export async function ArticleView({ articleId }: { articleId: string }) {
|
|
17
|
+
const article = await getArticleByIdAction(articleId);
|
|
18
|
+
if (!article) {
|
|
19
|
+
return <p>The requested article could not be found.</p>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<article>
|
|
24
|
+
<h1>{article.title}</h1>
|
|
25
|
+
<LikeButton articleId={article.id} initialLikes={article.likes} />
|
|
26
|
+
</article>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The Boundary Is on the Module Graph, Not the Render Tree
|
|
32
|
+
|
|
33
|
+
`'use client'` placed at the top of a file marks that **module and all of its static imports** as client code.
|
|
34
|
+
|
|
35
|
+
### Two Key Architectural Rules
|
|
36
|
+
|
|
37
|
+
1. **Props crossing into a Client Component must be serializable** (primitives, plain objects, arrays, `ReactNode`, or `'use server'` functions). Class instances, closures, and `Date`/`Map` instances (unless supported by the framework serializer) cannot cross as props.
|
|
38
|
+
2. **Client Components can render Server Components passed as `children` (Slot pattern).** Passing a Server Component via `children` keeps the server subtree on the server without pulling its dependencies into the client bundle:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
"use client";
|
|
42
|
+
|
|
43
|
+
import { useState, type ReactNode } from "react";
|
|
44
|
+
|
|
45
|
+
export function CollapsiblePanel({
|
|
46
|
+
title,
|
|
47
|
+
children,
|
|
48
|
+
}: {
|
|
49
|
+
title: string;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
}) {
|
|
52
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<section>
|
|
56
|
+
<button type="button" onClick={() => setIsOpen((prev) => !prev)}>
|
|
57
|
+
{title}
|
|
58
|
+
</button>
|
|
59
|
+
{isOpen && <div>{children}</div>}
|
|
60
|
+
</section>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## `'use server'` — Server Functions & Valibot Validation
|
|
66
|
+
|
|
67
|
+
`'use server'` marks a function as callable from the client but executed on the server. Treat every `'use server'` function as an **untrusted public HTTP endpoint**:
|
|
68
|
+
|
|
69
|
+
1. **Always validate inputs with Valibot** (`* as v from "valibot"`). Never use Zod.
|
|
70
|
+
2. **Always authenticate and authorize** the caller inside the function before mutating state.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
"use server";
|
|
74
|
+
|
|
75
|
+
import * as v from "valibot";
|
|
76
|
+
import { requireSessionUser } from "@/actions/auth/require-session-user.action";
|
|
77
|
+
import { createCommentAction } from "@/actions/comments/create-comment.action";
|
|
78
|
+
|
|
79
|
+
const publishCommentSchema = v.object({
|
|
80
|
+
text: v.pipe(
|
|
81
|
+
v.string("Please enter a comment."),
|
|
82
|
+
v.trim(),
|
|
83
|
+
v.minLength(2, "Your comment must be at least 2 characters long.")
|
|
84
|
+
),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export async function publishCommentServerFn(formData: FormData) {
|
|
88
|
+
const parsed = v.safeParse(publishCommentSchema, {
|
|
89
|
+
text: formData.get("text"),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!parsed.success) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
error: parsed.issues[0].message,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const user = await requireSessionUser();
|
|
100
|
+
await createCommentAction({ userId: user.id, text: parsed.output.text });
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
message: "Your comment has been published successfully.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Version Pinning in RSC Projects
|
|
110
|
+
|
|
111
|
+
RSC relies on bundler-facing internals that are not semver-stable across minor/patch skews. Pin exact matching versions of `react` and `react-dom` in `package.json`:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"dependencies": {
|
|
116
|
+
"react": "19.2.0",
|
|
117
|
+
"react-dom": "19.2.0"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Anti-Patterns
|
|
123
|
+
|
|
124
|
+
| Anti-pattern | Why it is wrong | Fix |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Calling `useState`/`useEffect` in a Server Component | Server Components do not execute on the client | Extract interactive leaf into a `'use client'` component |
|
|
127
|
+
| Importing server-only modules into a `'use client'` file | Pulls server code into the client bundle or fails build | Pass server data via serializable props or `children` slots |
|
|
128
|
+
| Trusting `'use server'` arguments without validation | Client payloads can be forged like any HTTP POST | Validate with Valibot (`v.safeParse`) and verify auth inside the function |
|
|
129
|
+
| Using caret ranges (`^19.0.0`) with RSC bundlers | Version skew between `react` and `react-dom` breaks RSC wiring | Pin exact matching versions |
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-core
|
|
3
|
+
description: Use when building or modifying React components, JSX, props, local state, core hooks (useState, useEffect, useRef, useContext, useMemo, useCallback), lists, conditional UI, or controlled inputs. TRIGGER when the project imports 'react' or 'react-dom', or when editing .tsx/.jsx files. Always adhere to the 'best-practices' skill and pair with 'react-advanced' for React 19 platform features and performance architecture.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Core Fundamentals & Opinionated Standards
|
|
7
|
+
|
|
8
|
+
Canonical reference for React fundamentals in GrooveSkills projects: function components, JSX, props and immutable state, core hooks, composition, conditional and list rendering, and controlled inputs.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Golden Rules (Non-negotiable)
|
|
13
|
+
|
|
14
|
+
1. **One Component per File & < 200 Lines**:
|
|
15
|
+
- Every exported React component resides in its own dedicated `.tsx` file and never exceeds **200 lines**.
|
|
16
|
+
- Extract sub-components or custom hooks (`use-*.ts`) into atomic files before reaching the limit.
|
|
17
|
+
2. **Mandatory Path Aliases (`@/...`) & Zero Barrel Files**:
|
|
18
|
+
- Import components, hooks, schemas, and utilities directly via path aliases (`@/components/ui/card`, `@/hooks/use-window-width`).
|
|
19
|
+
- Never create `index.ts` or `index.tsx` barrel files to re-export components or hooks.
|
|
20
|
+
3. **Strict Props Typing (Zero `any`)**:
|
|
21
|
+
- Define explicit `interface` or `type` declarations for component props.
|
|
22
|
+
- Treat `props` and `state` as strictly immutable.
|
|
23
|
+
4. **Early Returns for UI States (No Nested Ternaries)**:
|
|
24
|
+
- Handle loading, error, and empty states with top-level guard clauses (`if (!data) return <EmptyState />;`) instead of deeply nested JSX conditionals.
|
|
25
|
+
5. **Top-Level Hooks Only (Rules of Hooks)**:
|
|
26
|
+
- Call hooks unconditionally at the top level of function components or custom hooks—never inside loops, conditions, or nested functions (except React 19's `use()`, covered in `react-advanced`).
|
|
27
|
+
6. **Immutable State Updates & Functional Updaters**:
|
|
28
|
+
- Never mutate arrays or objects in place (`items.push()`). Always pass a new reference (`[...items, next]`).
|
|
29
|
+
- Use functional updaters (`setCount((prev) => prev + step)`) whenever next state depends on previous state.
|
|
30
|
+
7. **Compiler-Aware Memoization**:
|
|
31
|
+
- Do not wrap every value or callback in `useMemo` or `useCallback` by reflex. When the React Compiler is active, it memoizes automatically.
|
|
32
|
+
- Reserve manual `useMemo` and `useCallback` for stabilizing references passed to `useEffect` dependency arrays or for genuinely expensive computations in uncompiled modules.
|
|
33
|
+
8. **Stable Identity Keys in Lists**:
|
|
34
|
+
- Always provide a stable, unique `key` derived from domain identity (`item.id`), never the array index (`i`) for dynamic or reorderable lists.
|
|
35
|
+
9. **Controlled Inputs with Single Source of Truth**:
|
|
36
|
+
- Derive input `value` from state and handle updates via `onChange`, validating user input with **Valibot** (`* as v from "valibot"`) where applicable.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Components, JSX, Props, and Early Returns
|
|
41
|
+
|
|
42
|
+
Name components in `PascalCase`. Return a single root element or Fragment (`<>...</>`). Use guard clauses to keep the primary render path flat.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useState } from "react";
|
|
46
|
+
|
|
47
|
+
interface UserSummaryProps {
|
|
48
|
+
name: string;
|
|
49
|
+
role?: string;
|
|
50
|
+
isActive: boolean;
|
|
51
|
+
step?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function UserSummary({ name, role = "Member", isActive, step = 1 }: UserSummaryProps) {
|
|
55
|
+
const [count, setCount] = useState<number>(0);
|
|
56
|
+
|
|
57
|
+
if (!isActive) {
|
|
58
|
+
return <p className="text-muted">This account is currently inactive.</p>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<section className="user-summary">
|
|
63
|
+
<h2>Hello, {name} ({role})</h2>
|
|
64
|
+
<button type="button" onClick={() => setCount((prev) => prev + step)}>
|
|
65
|
+
Count: {count}
|
|
66
|
+
</button>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. Core Hooks Guide
|
|
75
|
+
|
|
76
|
+
- **`useState`**: Holds reactive local state (`const [isOpen, setIsOpen] = useState<boolean>(false);`).
|
|
77
|
+
- **`useEffect`**: Synchronizes with external systems (DOM listeners, timers, subscriptions). Always declare accurate dependencies and return a cleanup function.
|
|
78
|
+
- **`useRef`**: Holds a mutable value across renders without causing re-renders, or references a DOM node.
|
|
79
|
+
- **`useMemo` & `useCallback`**: Deliberate escape hatches to stabilize references for `useEffect` dependency arrays or heavy computations in uncompiled components.
|
|
80
|
+
- **`useContext`**: Reads shared state from a `Context` higher in the tree (`<ThemeContext value={theme}>` in React 19).
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { useEffect, useRef, useState } from "react";
|
|
84
|
+
|
|
85
|
+
export function AutoFocusWindowTracker() {
|
|
86
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
87
|
+
const [width, setWidth] = useState<number>(() => window.innerWidth);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
inputRef.current?.focus();
|
|
91
|
+
const handleResize = (): void => setWidth(window.innerWidth);
|
|
92
|
+
window.addEventListener("resize", handleResize);
|
|
93
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return <input ref={inputRef} placeholder={`Viewport: ${width}px`} />;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4. Composition, Lists, and Controlled Inputs
|
|
103
|
+
|
|
104
|
+
Favor composition via `children` over bloated configuration props, use stable domain keys (`todo.id`) for lists, and bind controlled inputs to state:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useState, type ReactNode } from "react";
|
|
108
|
+
|
|
109
|
+
export interface TodoItem {
|
|
110
|
+
id: string;
|
|
111
|
+
text: string;
|
|
112
|
+
done: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function TodoCard({
|
|
116
|
+
title,
|
|
117
|
+
todos,
|
|
118
|
+
children,
|
|
119
|
+
}: {
|
|
120
|
+
title: string;
|
|
121
|
+
todos: TodoItem[];
|
|
122
|
+
children?: ReactNode;
|
|
123
|
+
}) {
|
|
124
|
+
const [filter, setFilter] = useState<string>("");
|
|
125
|
+
|
|
126
|
+
if (todos.length === 0) {
|
|
127
|
+
return <p>Your task list is clear. Add a new item to get started.</p>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const visible = todos.filter((t) => t.text.toLowerCase().includes(filter.toLowerCase()));
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<article className="card">
|
|
134
|
+
<h2>{title}</h2>
|
|
135
|
+
<input
|
|
136
|
+
value={filter}
|
|
137
|
+
onChange={(event) => setFilter(event.target.value)}
|
|
138
|
+
placeholder="Filter tasks..."
|
|
139
|
+
/>
|
|
140
|
+
<ul>
|
|
141
|
+
{visible.map((todo) => (
|
|
142
|
+
<li key={todo.id}>{todo.done ? <s>{todo.text}</s> : todo.text}</li>
|
|
143
|
+
))}
|
|
144
|
+
</ul>
|
|
145
|
+
{children}
|
|
146
|
+
</article>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 5. Anti-Patterns to Avoid
|
|
154
|
+
|
|
155
|
+
| Anti-Pattern | Why It Fails | Correct Pattern |
|
|
156
|
+
| :--- | :--- | :--- |
|
|
157
|
+
| Mutating state in place (`items.push(next)`) | Same array reference; React skips re-render | Create a new reference: `setItems((prev) => [...prev, next])` |
|
|
158
|
+
| Calling hooks after `if` or inside loops | Breaks React's deterministic hook call order | Call hooks unconditionally at the top level; branch on results |
|
|
159
|
+
| Using array index `key={i}` on dynamic lists | Misattributes component state on insert, delete, or sort | Use a stable domain identifier: `key={item.id}` |
|
|
160
|
+
| Fetching or deriving synchronous state inside `useEffect` | Triggers an extra render pass and causes UI flicker | Derive values inline during render; use `use()` / Suspense or Actions for async data (`react-advanced`) |
|
|
161
|
+
| Re-exporting components via `components/index.ts` | Violates Zero Barrel Files and harms tree-shaking | Import directly from `@/components/ui/card` |
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 6. Sibling Skills
|
|
166
|
+
|
|
167
|
+
- **`best-practices`**: Universal clean architecture, `< 200` lines limit, `@/` path aliases, strict typing, and courteous user feedback.
|
|
168
|
+
- **`react-advanced`**: React 19 platform features, React Compiler 1.0 auto-memoization, concurrent rendering (`useTransition`, `useDeferredValue`, `Suspense`), `use()`, Actions (`useActionState`, `useOptimistic`, `useFormStatus`), context optimization, virtualization, and RSC boundaries.
|