@wangs-ui/skills 1.0.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/dist/bin.js +84 -0
- package/dist/index.js +2 -0
- package/dist/skills/create-form/SKILL.md +67 -0
- package/dist/skills/data-table/SKILL.md +68 -0
- package/dist/skills/dialog-modal/SKILL.md +58 -0
- package/dist/skills/i18n-usage/SKILL.md +232 -0
- package/dist/skills/layout-navigation/SKILL.md +62 -0
- package/dist/skills/react19-compiler-typescript/SKILL.md +388 -0
- package/dist/skills/typescript-strict-typing/SKILL.md +317 -0
- package/dist/skills/wangs-ui-components/SKILL.md +108 -0
- package/dist/src-BsIrKDsV.js +244 -0
- package/package.json +58 -0
- package/skills/create-form/SKILL.md +67 -0
- package/skills/data-table/SKILL.md +68 -0
- package/skills/dialog-modal/SKILL.md +58 -0
- package/skills/i18n-usage/SKILL.md +232 -0
- package/skills/layout-navigation/SKILL.md +62 -0
- package/skills/react19-compiler-typescript/SKILL.md +388 -0
- package/skills/typescript-strict-typing/SKILL.md +317 -0
- package/skills/wangs-ui-components/SKILL.md +108 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: i18n-usage
|
|
3
|
+
description: Comprehensive guidelines for application internationalization, JIT translations (t), ICU formatting, and locale-aware formatting with @wangs-ui/react-i18n.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Application Internationalization & Formatting Protocol
|
|
7
|
+
|
|
8
|
+
Use this skill when implementing multi-language interfaces, translating user-facing text, formatting dates, times, currencies, or numbers in React applications built with Wangs UI and `@wangs-ui/react-i18n`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. The MCP Discovery Protocol (Single Source of Truth)
|
|
13
|
+
|
|
14
|
+
Do **NOT** guess component localization contracts, language switcher variants, or datepicker props. Query the MCP server dynamically to inspect exact props and live story implementations:
|
|
15
|
+
|
|
16
|
+
### Inspect Localized Component Contracts:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
get-documentation({ "id": "languageswitcher" })
|
|
20
|
+
get-documentation({ "id": "currencyinput" })
|
|
21
|
+
get-documentation({ "id": "datepicker" })
|
|
22
|
+
get-documentation({ "id": "select" })
|
|
23
|
+
get-documentation({ "id": "datatable" })
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Inspect Live Story Implementations:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
get-documentation-for-story({ "id": "languageswitcher", "storyName": "Basic" })
|
|
30
|
+
get-documentation-for-story({ "id": "currencyinput", "storyName": "Basic" })
|
|
31
|
+
get-documentation-for-story({ "id": "datepicker", "storyName": "Default" })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Root Provider Setup (`WangsUiI18nProvider`)
|
|
37
|
+
|
|
38
|
+
Wrap the application root with `WangsUiI18nProvider` from `@wangs-ui/react-i18n` to enable dynamic JIT translations, versioned cache invalidation, and locale context:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { WangsUiI18nProvider } from '@wangs-ui/react-i18n';
|
|
42
|
+
import React from 'react';
|
|
43
|
+
import ReactDOM from 'react-dom/client';
|
|
44
|
+
import App from './App';
|
|
45
|
+
|
|
46
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
47
|
+
<React.StrictMode>
|
|
48
|
+
<WangsUiI18nProvider defaultLocale="en" baseUrl={import.meta.env.VITE_API_URL || ''}>
|
|
49
|
+
<App />
|
|
50
|
+
</WangsUiI18nProvider>
|
|
51
|
+
</React.StrictMode>,
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. Translation Protocol with `useI18n()`
|
|
58
|
+
|
|
59
|
+
The `@wangs-ui/react-i18n` package uses a Just-In-Time (JIT) translation architecture where natural English text strings serve as database keys.
|
|
60
|
+
|
|
61
|
+
### A. Consumer-Level Translation for `ReactNode` Props (Mandatory)
|
|
62
|
+
|
|
63
|
+
All user-facing text props in Wangs UI components (`placeholder`, `label`, `emptyMessage`, `header`, `tooltip`, etc.) are typed as `ReactNode` and rendered as-is. Components do **NOT** automatically translate custom strings. Translation **MUST** be called at the application/consumer level:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { useI18n } from '@wangs-ui/react-i18n';
|
|
67
|
+
import Button from '@wangs-ui/react-core/primitive/button';
|
|
68
|
+
import DataTable from '@wangs-ui/react-core/primitive/datatable';
|
|
69
|
+
import Select from '@wangs-ui/react-core/primitive/select';
|
|
70
|
+
|
|
71
|
+
export function OrderList() {
|
|
72
|
+
const { t } = useI18n();
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<Select placeholder={t('Search category...')} />
|
|
77
|
+
<DataTable emptyMessage={t('No orders found')} />
|
|
78
|
+
<Button label={t('Create new order')} />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### B. Natural English Sentence Keys
|
|
85
|
+
|
|
86
|
+
Always write full, natural English sentences as translation keys. Never use artificial dotted namespace keys:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// ✅ Good — Natural English
|
|
90
|
+
t('Invoice Summary');
|
|
91
|
+
t('Are you sure you want to delete this customer?');
|
|
92
|
+
|
|
93
|
+
// ❌ Bad — Artificial dotted keys
|
|
94
|
+
t('invoice.summary.title');
|
|
95
|
+
t('dialog.delete.customer.confirm');
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### C. Named Variable Interpolation (Single Braces `{var}`)
|
|
99
|
+
|
|
100
|
+
Pass interpolation values inside a plain object using descriptive named variables. This provides crucial semantic context for AI translation engines:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// ✅ Good — Named variables provide context
|
|
104
|
+
t('Upload {count} files to {groupName}', { count: 5, groupName: 'Marketing' });
|
|
105
|
+
t('Welcome back, {userName}!', { userName: user.name });
|
|
106
|
+
|
|
107
|
+
// ❌ Bad — Concatenation or positional arguments
|
|
108
|
+
t('Welcome back, ' + user.name);
|
|
109
|
+
t('Upload {0} files to {1}', 5, 'Marketing');
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### D. ICU Pluralization & Zero-State (`=0`)
|
|
113
|
+
|
|
114
|
+
Always handle singular, plural, and zero states directly within ICU MessageFormat strings. Do **NOT** use JavaScript ternary operators:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// ✅ Good — Clean ICU pluralization with zero-state handling
|
|
118
|
+
t('{count, plural, =0 {No items selected} one {1 item selected} other {{count} items selected}}', {
|
|
119
|
+
count: selectedCount,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ❌ Bad — Manual JS branching
|
|
123
|
+
selectedCount === 0
|
|
124
|
+
? t('No items selected')
|
|
125
|
+
: selectedCount === 1
|
|
126
|
+
? t('1 item selected')
|
|
127
|
+
: t('{count} items selected', { count: selectedCount });
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### E. Rich Text / Annotated Strings
|
|
131
|
+
|
|
132
|
+
Use standard supported HTML tags (`<a>`, `<b>`, `<i>`, `<u>`, `<s>`, `<br/>`, `<sub>`, `<sup>`, `<code>`, `<mark>`) for inline styling. Tags are automatically parsed into React elements without custom regex or string manipulation:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import Link from '@wangs-ui/foundation/theme/Link';
|
|
136
|
+
|
|
137
|
+
t('You have selected <b>{count} items</b>. Click <a>here</a> to review.', {
|
|
138
|
+
count: selectedCount,
|
|
139
|
+
a: (chunks) => <Link href="/review">{chunks}</Link>,
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 4. Locale Formatting Protocol with `useLocaleFormatter()`
|
|
146
|
+
|
|
147
|
+
For locale-aware formatting of dates, relative times, currencies, numbers, and display names, use the dedicated `useLocaleFormatter()` hook. All functions automatically adapt to the active locale without triggering backend database requests:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { useLocaleFormatter } from '@wangs-ui/react-i18n';
|
|
151
|
+
|
|
152
|
+
export function SummaryCard({ updatedAt, amount, count }: Props) {
|
|
153
|
+
const {
|
|
154
|
+
formatDate,
|
|
155
|
+
formatRelativeTime,
|
|
156
|
+
formatCurrency,
|
|
157
|
+
formatNumber,
|
|
158
|
+
formatDisplayName,
|
|
159
|
+
formatList,
|
|
160
|
+
truncateText,
|
|
161
|
+
} = useLocaleFormatter();
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div>
|
|
165
|
+
{/* Date formatting with Go tokens or date-fns tokens, and timezone */}
|
|
166
|
+
<p>{formatDate(new Date(), 'dd MMMM yyyy, HH:mm', 'Asia/Jakarta')}</p>
|
|
167
|
+
|
|
168
|
+
{/* Relative time */}
|
|
169
|
+
<p>{formatRelativeTime(updatedAt)}</p>
|
|
170
|
+
|
|
171
|
+
{/* Currency formatting */}
|
|
172
|
+
<p>{formatCurrency(amount, 'IDR')}</p>
|
|
173
|
+
|
|
174
|
+
{/* Number formatting with locale grouping */}
|
|
175
|
+
<p>{formatNumber(count)}</p>
|
|
176
|
+
|
|
177
|
+
{/* ISO code to localized name */}
|
|
178
|
+
<p>{formatDisplayName('id', 'language')}</p>
|
|
179
|
+
|
|
180
|
+
{/* Localized list */}
|
|
181
|
+
<p>{formatList(['Finance', 'Operations', 'IT'])}</p>
|
|
182
|
+
|
|
183
|
+
{/* Emoji & multi-byte safe text truncation */}
|
|
184
|
+
<p>{truncateText('Long product description with emojis 🚀', 20)}</p>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> [!NOTE]
|
|
191
|
+
> Formatters MUST NOT be called as standalone `t()` keys (e.g. `t(formatRelativeTime(date))`). Instead, pass the formatted result as a named variable:
|
|
192
|
+
>
|
|
193
|
+
> ```tsx
|
|
194
|
+
> const { t } = useI18n();
|
|
195
|
+
> const { formatRelativeTime } = useLocaleFormatter();
|
|
196
|
+
> const label = t('Updated {time}', { time: formatRelativeTime(updatedAt) });
|
|
197
|
+
> ```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 5. Language Switching UI Integration
|
|
202
|
+
|
|
203
|
+
Connect the Wangs UI `LanguageSwitcher` primitive directly with `useI18n()` state:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import LanguageSwitcher from '@wangs-ui/react-core/primitive/languageswitcher';
|
|
207
|
+
import { useI18n } from '@wangs-ui/react-i18n';
|
|
208
|
+
|
|
209
|
+
export function HeaderLanguageSwitcher() {
|
|
210
|
+
const { locale, setLocale, languageOptions } = useI18n();
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<LanguageSwitcher
|
|
214
|
+
options={languageOptions}
|
|
215
|
+
value={locale}
|
|
216
|
+
onChange={(code) => setLocale(code)}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 6. Strict Behavioral Constraints (MUST NOT)
|
|
225
|
+
|
|
226
|
+
- **NO Formatters Destructured from `useI18n()`:** Formatters are isolated in `useLocaleFormatter()`. Never attempt to import `formatDate` or `formatCurrency` from `useI18n()`.
|
|
227
|
+
- **NO String Concatenation in `t()` Keys:** Never concatenate strings or use dynamic template literals (e.g. `t('Hello ' + user.name)` or ``t(`Hello ${user.name}`)``). This creates infinite distinct keys in the translation database and prevents caching.
|
|
228
|
+
- **NO Manual Zero-State JavaScript Branching:** Always use ICU `=0` syntax inside a single plural key.
|
|
229
|
+
- **NO Dotted Artificial Translation Keys:** Never use dotted keys like `t('app.header.title')`. Use natural English sentences.
|
|
230
|
+
- **NO Custom Markdown Formatting Symbols:** Do not use `*bold*` or `_italic_` in translation keys. Use valid HTML tags like `<b>bold</b>`.
|
|
231
|
+
- **NO Hardcoded Static Translation Dictionaries:** Do not bundle static translation files (`id.json`, `zh.json`). The JIT backend broker manages translations dynamically.
|
|
232
|
+
- **NO Unnecessary English Key Modifications:** Minor typos or punctuation changes in keys create orphaned entries in the translation backend and trigger new AI translation costs.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layout-navigation
|
|
3
|
+
description: Architecture, navigation hierarchies, and MCP discovery protocol for AppLayout, Sidebar, Breadcrumb, and Tabs in Wangs UI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: Application Layout & Navigation Hierarchy
|
|
7
|
+
|
|
8
|
+
Use this skill when constructing application shells, multi-level sidebars, page headers, breadcrumbs, or tabbed views with `@wangs-ui/react-core`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. MCP Inspection Protocol (Mandatory Single Source of Truth)
|
|
13
|
+
|
|
14
|
+
Do **NOT** guess layout block slots, sidebar item interfaces, or breadcrumb props. Query the MCP server dynamically to inspect exact contracts and live story implementations:
|
|
15
|
+
|
|
16
|
+
### Inspect Layout & Navigation Contracts:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
get-documentation({ "id": "applayout" })
|
|
20
|
+
get-documentation({ "id": "sidebar" })
|
|
21
|
+
get-documentation({ "id": "breadcrumb" })
|
|
22
|
+
get-documentation({ "id": "tabs" })
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Inspect Live Story Implementations:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
get-documentation-for-story({ "id": "applayout", "storyName": "Default" })
|
|
29
|
+
get-documentation-for-story({ "id": "sidebar", "storyName": "Default" })
|
|
30
|
+
get-documentation-for-story({ "id": "breadcrumb", "storyName": "Default" })
|
|
31
|
+
get-documentation-for-story({ "id": "tabs", "storyName": "Default" })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Inspect Knowledge Graph & Usages:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
query_graph({ "query": "AppLayout" })
|
|
38
|
+
query_graph({ "query": "Sidebar" })
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Layout Architecture & Mental Model
|
|
44
|
+
|
|
45
|
+
1. **Top-Level App Shell (`AppLayout`)**:
|
|
46
|
+
Provides structured slots for `sidebar`, `header`, and main content view, handling responsive viewport scaling and mobile navigation overlays.
|
|
47
|
+
2. **Hierarchical Menu (`Sidebar`)**:
|
|
48
|
+
Renders single and nested navigation items, active route indicators, collapsible state, and notification badges.
|
|
49
|
+
3. **Breadcrumb Trail (`Breadcrumb`)**:
|
|
50
|
+
Maintains clear navigational hierarchy on page headers.
|
|
51
|
+
4. **Tabbed Sub-Views (`Tabs`)**:
|
|
52
|
+
Organizes complex entity detail views or multi-section settings into distinct tabbed panels.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Mandatory Implementation Rules
|
|
57
|
+
|
|
58
|
+
1. **Query MCP for Current Code Patterns**: Inspect `applayout` and `sidebar` stories via MCP before assembling the layout.
|
|
59
|
+
2. **Strict Subpath Imports**: Import layout blocks via `@wangs-ui/react-core/blocks/*` and primitives via `@wangs-ui/react-core/primitive/*`.
|
|
60
|
+
3. **Consistent Spacing Grid**: Use standard container padding (`p-6` or `p-xxl`) across page contents.
|
|
61
|
+
4. **Page Hierarchy Alignment**: Every page view inside the layout must provide a clear `.heading-1` hierarchy and synchronized breadcrumbs.
|
|
62
|
+
5. **Translate Navigation Labels**: Wrap all sidebar item labels and breadcrumb texts in `t('...')` from `@wangs-ui/react-i18n`.
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react19-compiler-typescript
|
|
3
|
+
description: Enforce idiomatic React 19 + TypeScript conventions built around the React Compiler's automatic memoization. Use this any time writing, generating, reviewing, or refactoring React components, hooks, or props in TypeScript/TSX — including code that manually wraps things in useMemo/useCallback/React.memo, uses forwardRef, mutates props/state, or needs typing for Actions, useOptimistic, use(), or refs. Trigger even if the user didn't say "React 19" or "compiler" explicitly; it applies whenever React component/hook code is being written or optimized.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React 19 + TypeScript with the React Compiler
|
|
7
|
+
|
|
8
|
+
## Why this matters
|
|
9
|
+
|
|
10
|
+
React Compiler (stable since React Compiler 1.0, October 2025) rewrites your components
|
|
11
|
+
and hooks at build time, inserting memoization equivalent to `useMemo`/`useCallback`/
|
|
12
|
+
`React.memo` automatically and more granularly than a human would by hand. It ships as
|
|
13
|
+
`babel-plugin-react-compiler`, and its lint rules live inside `eslint-plugin-react-hooks`
|
|
14
|
+
(recommended preset) so linting and compilation share one source of truth.
|
|
15
|
+
|
|
16
|
+
The practical consequence: **manual memoization is no longer the default** — it's
|
|
17
|
+
either redundant, or actively harmful if it doesn't match what the compiler would have
|
|
18
|
+
inferred (the compiler bails out silently rather than risk breaking your app). Writing
|
|
19
|
+
"optimized" React in 2026 means writing _plain, rule-following_ React and trusting the
|
|
20
|
+
build step, not sprinkling `useMemo` everywhere out of habit.
|
|
21
|
+
|
|
22
|
+
This skill assumes and builds on the base `typescript-strict-typing` skill for general
|
|
23
|
+
typing discipline (no `any`, `interface` for entities, discriminated unions for variant
|
|
24
|
+
state, etc.) — apply both together.
|
|
25
|
+
|
|
26
|
+
## Core principle
|
|
27
|
+
|
|
28
|
+
> Write plain, obviously-pure React. Let the compiler memoize. The Rules of React are no
|
|
29
|
+
> longer just style guidance — the compiler's correctness depends on you following them.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Stop hand-rolling memoization
|
|
34
|
+
|
|
35
|
+
> ⚠️ **Everything in this section assumes the compiler is confirmed active** (wired per
|
|
36
|
+
> §7, verified via the "Memo ✨" badge in §8). If you drop manual memoization _without_
|
|
37
|
+
> that confirmation, you don't get automatic memoization to replace it — you get
|
|
38
|
+
> **neither**. That's not a correctness bug (React still renders the right output), but
|
|
39
|
+
> every child re-renders on every parent render regardless of whether its props
|
|
40
|
+
> actually changed, and every inline computation reruns every render with nothing
|
|
41
|
+
> caching it. It's the pre-memoization default behavior of React — often invisible in
|
|
42
|
+
> small trees, but a real source of jank in large lists, heavy computations, or deep
|
|
43
|
+
> trees under a frequently-re-rendering parent. If you're not certain the compiler is
|
|
44
|
+
> active yet, keep existing manual memoization until you've verified it, then remove it.
|
|
45
|
+
|
|
46
|
+
Don't reach for `useMemo`, `useCallback`, or `React.memo` by default — the compiler adds
|
|
47
|
+
this automatically wherever it determines it helps.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// ❌ Old habit — noisy, and a mismatched dependency array is a whole class of bugs
|
|
51
|
+
const filteredUsers = useMemo(() => users.filter((u) => u.isActive), [users]);
|
|
52
|
+
const handleClick = useCallback(() => onSelect(user.id), [onSelect, user.id]);
|
|
53
|
+
|
|
54
|
+
// ✅ New default — just write the logic; the compiler memoizes what's worth memoizing
|
|
55
|
+
const filteredUsers = users.filter((u) => u.isActive);
|
|
56
|
+
const handleClick = () => onSelect(user.id);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Manual memoization is still justified, narrowly, when:
|
|
60
|
+
|
|
61
|
+
- You've **confirmed a compiler bail-out** (see §6) on a genuine hot path via profiling,
|
|
62
|
+
and fixing the underlying Rules-of-React violation isn't possible right now.
|
|
63
|
+
- A value must have **stable referential identity across a boundary the compiler can't
|
|
64
|
+
see** — e.g. passed into a non-React library, a WebSocket subscription, or a
|
|
65
|
+
third-party hook incompatible with the compiler (`react-hook-form`'s `useForm`,
|
|
66
|
+
`@tanstack/react-table`'s `useReactTable` are known cases).
|
|
67
|
+
- Keep any manual memoization it produces isolated and commented with _why_, so it
|
|
68
|
+
doesn't silently rot into a bail-out later when the code around it changes.
|
|
69
|
+
|
|
70
|
+
## 2. The Rules of React are now load-bearing
|
|
71
|
+
|
|
72
|
+
The compiler assumes your components and hooks are pure. Violating these rules doesn't
|
|
73
|
+
just risk a subtle bug anymore — it causes the compiler to silently skip optimizing that
|
|
74
|
+
component:
|
|
75
|
+
|
|
76
|
+
- **Idempotent renders** — given the same props/state/context, a component must return
|
|
77
|
+
the same output. No random values, no `Date.now()`, no side effects during render.
|
|
78
|
+
- **Immutability** — never mutate props, state, or context directly. Always create new
|
|
79
|
+
objects/arrays for changes.
|
|
80
|
+
- **Side effects only in effects or event handlers** — never during render.
|
|
81
|
+
- **Hooks called unconditionally, top-level, same order every render** — no hooks inside
|
|
82
|
+
conditionals, loops, or nested functions.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// ❌ Mutates a prop — breaks purity and the compiler can't safely memoize this
|
|
86
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
87
|
+
todos.sort((a, b) => a.priority - b.priority); // mutates caller's array
|
|
88
|
+
return (
|
|
89
|
+
<ul>
|
|
90
|
+
{todos.map((t) => (
|
|
91
|
+
<li key={t.id}>{t.title}</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ✅ Creates a new array — pure, compiler-safe
|
|
98
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
99
|
+
const sorted = [...todos].sort((a, b) => a.priority - b.priority);
|
|
100
|
+
return (
|
|
101
|
+
<ul>
|
|
102
|
+
{sorted.map((t) => (
|
|
103
|
+
<li key={t.id}>{t.title}</li>
|
|
104
|
+
))}
|
|
105
|
+
</ul>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 3. Naming conventions the compiler relies on
|
|
111
|
+
|
|
112
|
+
The compiler identifies what to optimize by naming heuristics, same as the Rules of
|
|
113
|
+
Hooks linter:
|
|
114
|
+
|
|
115
|
+
| Kind | Convention | Notes |
|
|
116
|
+
| ------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------- |
|
|
117
|
+
| Components | `PascalCase`, returns JSX | Compiler treats it as a component to optimize |
|
|
118
|
+
| Custom hooks | `camelCase`, prefixed `use` | Required for both Rules-of-Hooks lint and compiler analysis |
|
|
119
|
+
| Plain helper functions that return JSX-like values but aren't components | Avoid `PascalCase`/`use` naming | Prevents the compiler (and other devs) from mistaking it for a component/hook |
|
|
120
|
+
|
|
121
|
+
## 4. Typing React 19 primitives
|
|
122
|
+
|
|
123
|
+
**`ref` as a normal prop** — `forwardRef` is no longer required for most cases; function
|
|
124
|
+
components can accept `ref` directly.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
type InputProps = {
|
|
128
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
129
|
+
placeholder?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function TextInput({ ref, placeholder }: InputProps) {
|
|
133
|
+
return <input ref={ref} placeholder={placeholder} />;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Actions with `useActionState`** — type the state and payload as generics; model the
|
|
138
|
+
result as a discriminated union (per the base typing skill) rather than optional fields.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
type FormState = { status: 'idle' } | { status: 'error'; message: string } | { status: 'success' };
|
|
142
|
+
|
|
143
|
+
const [state, formAction, isPending] = useActionState<FormState, FormData>(
|
|
144
|
+
async (_previous, formData) => {
|
|
145
|
+
const email = formData.get('email');
|
|
146
|
+
if (typeof email !== 'string' || !email.includes('@')) {
|
|
147
|
+
return { status: 'error', message: 'Invalid email' };
|
|
148
|
+
}
|
|
149
|
+
await submit(email);
|
|
150
|
+
return { status: 'success' };
|
|
151
|
+
},
|
|
152
|
+
{ status: 'idle' },
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Optimistic updates with `useOptimistic`** — type both the state and the update shape.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], Todo>(
|
|
160
|
+
todos,
|
|
161
|
+
(state, newTodo) => [...state, newTodo],
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Reading a promise or context with `use()`** — type the resolved value, not the
|
|
166
|
+
promise wrapper; `use()` is not a hook and may be called conditionally.
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
|
|
170
|
+
const comments = use(commentsPromise); // suspends until resolved
|
|
171
|
+
return (
|
|
172
|
+
<ul>
|
|
173
|
+
{comments.map((c) => (
|
|
174
|
+
<li key={c.id}>{c.text}</li>
|
|
175
|
+
))}
|
|
176
|
+
</ul>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Stable event callbacks with `useEffectEvent`** (React 19.2+) — separates "event"
|
|
182
|
+
logic from "reactive" effect logic so the callback always sees the latest props/state
|
|
183
|
+
without being listed as an effect dependency. Needs `eslint-plugin-react-hooks@6+` to
|
|
184
|
+
lint correctly.
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
const onVisit = useEffectEvent((url: string) => {
|
|
188
|
+
logVisit(url, theme); // always fresh `theme`, never re-triggers the effect
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
onVisit(url);
|
|
193
|
+
}, [url]); // `theme` intentionally omitted — onVisit is stable
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 5. Compiler-friendly render patterns
|
|
197
|
+
|
|
198
|
+
- Creating new object/array/function literals inline in render (`style={{ color }}`,
|
|
199
|
+
`onClick={() => ...}`) is fine — stop manually hoisting or `useMemo`-wrapping these
|
|
200
|
+
preemptively; the compiler memoizes them if it determines it's worthwhile.
|
|
201
|
+
- Avoid module-level mutable variables read or written during render — that state is
|
|
202
|
+
invisible to the compiler and breaks idempotence.
|
|
203
|
+
- Don't use `useRef` to store a value that should trigger a re-render when it changes —
|
|
204
|
+
refs are an imperative escape hatch, not state, and the compiler treats them as such.
|
|
205
|
+
- Keep components small and composable. The compiler optimizes per component/hook
|
|
206
|
+
boundary, so a single 300-line component gives it far less to work with than several
|
|
207
|
+
focused ones.
|
|
208
|
+
|
|
209
|
+
## 6. Typing props (builds on `typescript-strict-typing`)
|
|
210
|
+
|
|
211
|
+
- `interface` for a component's `Props` — it's an entity shape, often extended.
|
|
212
|
+
- A discriminated union when a component has mutually exclusive prop combinations,
|
|
213
|
+
instead of a pile of optional props that can contradict each other.
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// ❌ Bad — nothing stops passing both `href` and `onClick` incoherently
|
|
217
|
+
interface ButtonProps {
|
|
218
|
+
label: string;
|
|
219
|
+
href?: string;
|
|
220
|
+
onClick?: () => void;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ✅ Good — the two variants can't be mixed
|
|
224
|
+
type ButtonProps =
|
|
225
|
+
| { variant: 'link'; label: string; href: string }
|
|
226
|
+
| { variant: 'action'; label: string; onClick: () => void };
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 7. Tooling setup
|
|
230
|
+
|
|
231
|
+
**The compiler is opt-in — no default setup enables it automatically.** Plain
|
|
232
|
+
`@vitejs/plugin-react` (`react()`), plain Next.js, plain Babel/webpack config, etc. do
|
|
233
|
+
**not** run the compiler on their own. Verify it's actually wired up before assuming any
|
|
234
|
+
of the memoization guidance above applies to your build.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
# Compiler (build-time transform)
|
|
238
|
+
npm install --save-dev --save-exact babel-plugin-react-compiler@latest
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Lint rules — oxlint.** Oxlint ships a **native, Rust-based** `react/react-compiler`
|
|
242
|
+
rule that runs the same compiler analysis in lint-only mode — same diagnostics as the
|
|
243
|
+
Babel-based ESLint version, no Babel needed for linting. It's experimental and **off by
|
|
244
|
+
default**, so it has to be enabled explicitly:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
// .oxlintrc.json
|
|
248
|
+
{
|
|
249
|
+
"plugins": ["react"],
|
|
250
|
+
"rules": {
|
|
251
|
+
"react/react-compiler": "error"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This single rule reports two distinct things — both worth fixing, but for different
|
|
257
|
+
reasons:
|
|
258
|
+
|
|
259
|
+
- **Rules-of-React violations** (conditional hooks, reading a ref during render, mutating
|
|
260
|
+
props) — these are real bugs, independent of the compiler.
|
|
261
|
+
- **Compiler bail-outs** — places the compiler declined to optimize (e.g. unsupported
|
|
262
|
+
syntax) without a rule violation. Not incorrect code, just a missed optimization —
|
|
263
|
+
lower priority than a violation, but worth knowing about on a hot path.
|
|
264
|
+
|
|
265
|
+
If you'd rather use an existing ESLint plugin's rules through oxlint instead of the
|
|
266
|
+
native one (e.g. to match a team convention), oxlint's `jsPlugins` can load
|
|
267
|
+
`eslint-plugin-react-hooks` directly — slower than the native rule since it still runs
|
|
268
|
+
through Babel, but useful if you need a rule the native port doesn't cover yet:
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"jsPlugins": [{ "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" }],
|
|
273
|
+
"rules": { "react-hooks-js/set-state-in-render": "error" }
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Lint rules — ESLint** (if not on oxlint): the same rules ship inside
|
|
278
|
+
`eslint-plugin-react-hooks`.
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
npm install --save-dev eslint-plugin-react-hooks@latest
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
// eslint.config.js
|
|
286
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
287
|
+
import { defineConfig } from 'eslint/config';
|
|
288
|
+
|
|
289
|
+
export default defineConfig([reactHooks.configs.flat.recommended]);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Wiring it into Vite 8.** `@vitejs/plugin-react` v6+ (the version that ships with Vite 8) switched its default transform from Babel to oxc for speed, so the compiler is
|
|
293
|
+
**never** on by default and the old `react({ babel: {...} })` option **does not work**
|
|
294
|
+
on this setup — it's silently ignored, not an error, which is an easy way to think the
|
|
295
|
+
compiler is running when it isn't. Wire it in explicitly, as a separate Babel pass that
|
|
296
|
+
runs before `react()`:
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
// vite.config.js
|
|
300
|
+
import { defineConfig } from 'vite';
|
|
301
|
+
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
|
302
|
+
import babel from '@rolldown/plugin-babel';
|
|
303
|
+
|
|
304
|
+
export default defineConfig({
|
|
305
|
+
plugins: [
|
|
306
|
+
babel({ presets: [reactCompilerPreset()] }), // must run before react()
|
|
307
|
+
react(),
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
npm install --save-dev @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
|
|
314
|
+
npm install --save-dev @types/babel__core # if using TypeScript
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`reactCompilerPreset()` is a helper exported from `@vitejs/plugin-react` itself — it
|
|
318
|
+
bundles `babel-plugin-react-compiler` with sane default include/exclude filters so you
|
|
319
|
+
don't have to hand-roll a Babel preset. It optionally accepts:
|
|
320
|
+
|
|
321
|
+
- `compilationMode: 'annotation'` — only compile components explicitly marked with a
|
|
322
|
+
`"use memo"` directive, instead of the whole codebase (useful for a gradual rollout).
|
|
323
|
+
- `target: '17' | '18'` — if any part of the app still runs on an older React major and
|
|
324
|
+
needs the `react-compiler-runtime` package instead of `react/compiler-runtime`.
|
|
325
|
+
|
|
326
|
+
After adding this, confirm it's actually active via the React DevTools "Memo ✨" badge
|
|
327
|
+
(§8) before trusting the "don't hand-roll memoization" guidance in §1 — a silently
|
|
328
|
+
misconfigured Babel order (`react()` before `babel()`) is a common way for this to look
|
|
329
|
+
wired up but do nothing.
|
|
330
|
+
|
|
331
|
+
- Treat compiler-related lint errors (Rules-of-React violations, mismatched manual
|
|
332
|
+
memoization) as must-fix, not optional — an unfixed violation means that component
|
|
333
|
+
silently gets **zero** compiler optimization.
|
|
334
|
+
- For a large existing codebase, adopt incrementally by scoping the babel plugin to a
|
|
335
|
+
directory (e.g. a UI component library) before enabling it globally.
|
|
336
|
+
- If a specific function is genuinely incompatible with the compiler (e.g. it calls
|
|
337
|
+
`useForm` from `react-hook-form`), opt it out with the `"use no memo"` directive as
|
|
338
|
+
the **first line of the function body** — it's a temporary escape hatch, not a
|
|
339
|
+
permanent fix, so leave a comment explaining why.
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
function LegacyForm() {
|
|
343
|
+
'use no memo';
|
|
344
|
+
const form = useForm(); // incompatible with the compiler today
|
|
345
|
+
// ...
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## 8. Checking whether the compiler is actually optimizing
|
|
350
|
+
|
|
351
|
+
- **React DevTools** — an optimized component shows a "Memo ✨" badge next to its name
|
|
352
|
+
in the component tree.
|
|
353
|
+
- **ESLint** — the compiler's recommended rules flag Rules-of-React violations at lint
|
|
354
|
+
time, before they ever become a silent runtime bail-out.
|
|
355
|
+
- A bail-out is not a crash — it just means that specific component/hook is running
|
|
356
|
+
unoptimized. Treat a missing "Memo ✨" badge on a component you expect to be optimized
|
|
357
|
+
as a signal to check for a Rules-of-React violation, not a compiler bug.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Review checklist
|
|
362
|
+
|
|
363
|
+
- [ ] Compiler confirmed active ("Memo ✨" badge) before removing any _existing_ manual
|
|
364
|
+
memoization — don't strip it on faith
|
|
365
|
+
- [ ] No new `useMemo`/`useCallback`/`React.memo` added without a documented reason
|
|
366
|
+
(confirmed bail-out, or a boundary the compiler can't see through)
|
|
367
|
+
- [ ] No prop/state/context mutation anywhere in render
|
|
368
|
+
- [ ] All hooks called unconditionally at the top level, same order every render
|
|
369
|
+
- [ ] Side effects live in `useEffect`/event handlers, never during render
|
|
370
|
+
- [ ] Components are `PascalCase`; hooks are `camelCase` and prefixed `use`
|
|
371
|
+
- [ ] `ref` accepted as a normal prop instead of `forwardRef`, unless targeting a version
|
|
372
|
+
that requires it
|
|
373
|
+
- [ ] Action/optimistic-update state modeled as a discriminated union, not optional
|
|
374
|
+
fields
|
|
375
|
+
- [ ] Mutually exclusive prop combinations modeled as a discriminated union `Props` type
|
|
376
|
+
- [ ] `eslint-plugin-react-hooks` recommended config enabled and passing
|
|
377
|
+
- [ ] Any `"use no memo"` usage has a comment explaining why
|
|
378
|
+
|
|
379
|
+
## Quick reference
|
|
380
|
+
|
|
381
|
+
| Situation | Do |
|
|
382
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
383
|
+
| Tempted to write `useMemo`/`useCallback` | Don't — write the plain expression, let the compiler decide |
|
|
384
|
+
| Need a ref on a function component | Accept `ref` as a prop, skip `forwardRef` |
|
|
385
|
+
| Form/async state with distinct outcomes | Discriminated union via `useActionState`, not optional fields |
|
|
386
|
+
| Callback needs latest props/state without re-running an effect | `useEffectEvent` |
|
|
387
|
+
| A hook/library is known-incompatible with the compiler | `"use no memo"` at the top of that function, with a comment |
|
|
388
|
+
| Checking if optimization is happening | React DevTools "Memo ✨" badge + compiler ESLint rules |
|