Skip to content

Commit d24d642

Browse files
authored
chore: Update React Compiler adoption guidelines (#998)
This file is automatically synced from the `shared-configs` repository. Source: https://github.com/doist/shared-configs/blob/main/
1 parent ee3b106 commit d24d642

File tree

1 file changed

+130
-3
lines changed

1 file changed

+130
-3
lines changed

docs/react-compiler.md

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,71 @@ const handleTaskEditClick = useEvent((task: Task) => {
7676
})
7777
```
7878

79+
#### Alternative: useState lazy initialization
80+
81+
For values that need to be computed once (like store instances or initial values), use `useState` with a lazy initializer instead of the ref pattern.
82+
83+
**Before (store creation):**
84+
85+
```typescript
86+
const storeRef = useRef<TaskHierarchyStore>()
87+
if (!storeRef.current) {
88+
storeRef.current = store ?? createTaskHierarchyStore()
89+
}
90+
```
91+
92+
**After (store creation):**
93+
94+
```typescript
95+
const [storeInstance] = useState<TaskHierarchyStore>(() => store ?? createTaskHierarchyStore())
96+
```
97+
98+
**Note:** The lazy initializer captures the initial prop value. If `store` is initially undefined but becomes defined later, `storeInstance` will remain the created store. This matches the original ref behavior and is typically intentional.
99+
100+
**Before (one-time value computation):**
101+
102+
```typescript
103+
const placeholder = useMemo(
104+
() => getPlaceholder(completedTaskCount),
105+
// eslint-disable-next-line react-hooks/exhaustive-deps
106+
[],
107+
)
108+
```
109+
110+
**After (one-time value computation):**
111+
112+
```typescript
113+
const [placeholder] = useState(() => getPlaceholder(completedTaskCount))
114+
```
115+
116+
#### Storing refs in state
117+
118+
Storing refs in state and comparing `.current` values during render is also a violation of this rule. Instead, use a string or enum identifier to track which element is selected.
119+
120+
**Before:**
121+
122+
```typescript
123+
const [selectedInputRef, setSelectedInputRef] = useState(() =>
124+
initialFocus === 'description' ? richTextDescriptionRef : richTextContentInputRef,
125+
)
126+
127+
// During render:
128+
const isTitleInputFocused = selectedInputRef.current === richTextContentInputRef.current
129+
```
130+
131+
**After:**
132+
133+
```typescript
134+
type SelectedInputType = 'content' | 'description'
135+
136+
const [selectedInput, setSelectedInput] = useState<SelectedInputType>(() =>
137+
initialFocus === 'description' ? 'description' : 'content',
138+
)
139+
140+
// During render:
141+
const isTitleInputFocused = selectedInput === 'content'
142+
```
143+
79144
### Mismatched `useMemo` dependencies
80145

81146
> Reason: Existing memoization could not be preserved
@@ -221,19 +286,81 @@ const hasCompletedTasks = useMemo(() => {
221286
}, [transformedState])
222287
```
223288

289+
### Conditional hook calls
290+
291+
> Reason: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
292+
293+
The `store?.useState()` pattern conditionally calls a hook - when `store` is `undefined`, the hook isn't called; when defined, it is. This violates the Rules of Hooks which require hooks to be called unconditionally in the same order on every render.
294+
295+
**Before:**
296+
297+
```typescript
298+
const renderedItems = store?.useState('renderedItems')
299+
```
300+
301+
**After:**
302+
303+
```typescript
304+
import { useStoreState } from '@ariakit/react'
305+
306+
const renderedItems = useStoreState(store, 'renderedItems')
307+
```
308+
309+
The `store.useState()` pattern is from older versions of AriaKit. Since [version 0.4.9](https://ariakit.org/changelog#new-usestorestate-hook), AriaKit provides [`useStoreState`](https://ariakit.org/reference/use-store-state) which accepts stores that are null or undefined, returning undefined in those cases. The same principle applies to any conditional hook call - use an API that handles the conditional case internally.
310+
311+
### Function declaration order
312+
313+
> Reason: Cannot access variable before it is declared
314+
>
315+
> handleFormInputEnter is accessed before it is declared, which prevents the earlier access from updating when this value changes over time
316+
317+
or
318+
319+
> Reason: [PruneHoistedContexts] Rewrite hoisted function references
320+
321+
The compiler analyzes function dependencies statically. Functions must be declared before they're referenced by other functions.
322+
323+
**Before:**
324+
325+
```typescript
326+
function handleFormInputEnter(event: KeyboardEvent) {
327+
onChange?.(getCurrentEditorValue())
328+
handleFormSubmit(event) // Error: accessed before declaration
329+
}
330+
331+
function handleFormSubmit(event: KeyboardEvent | React.MouseEvent) {
332+
// ...
333+
}
334+
```
335+
336+
**After:**
337+
338+
```typescript
339+
function handleFormSubmit(event: KeyboardEvent | React.MouseEvent) {
340+
// ...
341+
}
342+
343+
function handleFormInputEnter(event: KeyboardEvent) {
344+
onChange?.(getCurrentEditorValue())
345+
handleFormSubmit(event) // Now valid
346+
}
347+
```
348+
224349
## Verifying fixes (for LLMs)
225350

226351
When fixing violations programmatically, use one of these methods to verify the fix was successful:
227352

228353
**1. CLI Tool (recommended)**
229354

230-
Run the tracker to check if a file has violations:
355+
Run the tracker with `--overwrite` to regenerate the records file, then check if the entry remains or if its error count has reduced:
231356

232357
```bash
233-
npx @doist/react-compiler-tracker --check-files src/path/to/file.tsx
358+
npx @doist/react-compiler-tracker --overwrite
234359
```
235360

236-
A successful check produces no output. If violations remain, the tool reports the error count.
361+
Compare `.react-compiler.rec.json` before and after: if the file's entry is removed or its error count decreased, the fix was successful.
362+
363+
**Note:** This runs against the entire codebase, not just the modified file. The workaround is needed because `--check-files` doesn't report resolved errors ([react-compiler-tracker#35](https://github.com/Doist/react-compiler-tracker/issues/35)).
237364

238365
**2. Babel with Inline Logger**
239366

0 commit comments

Comments
 (0)