Skip to content

Commit 4aafb87

Browse files
committed
chore: Update React development guidelines
This file is automatically synced from the `shared-configs` repository. Source: https://github.com/doist/shared-configs/blob/main/
1 parent df8a0be commit 4aafb87

File tree

5 files changed

+1652
-0
lines changed

5 files changed

+1652
-0
lines changed

docs/react-guidelines/async.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# React Async Patterns
2+
3+
## Race Conditions
4+
5+
Use `AbortController` and `useEffect` cleanup to cancel stale operations.
6+
7+
```typescript
8+
useEffect(
9+
function fetchProjectData() {
10+
const controller = new AbortController()
11+
12+
api.getProject(projectId, { signal: controller.signal })
13+
.then((data) => {
14+
setProject(data)
15+
})
16+
.catch((error) => {
17+
if (isAbortError(error?.cause ?? error)) {
18+
return // expected: request was cancelled
19+
}
20+
setError(error)
21+
})
22+
23+
return function abortFetchProjectData() {
24+
controller.abort()
25+
}
26+
},
27+
[projectId],
28+
)
29+
```
30+
31+
When a dependency changes before the previous request completes, the cleanup function aborts the stale request, preventing updates from out-of-order responses.
32+
33+
## Data Fetching
34+
35+
- Fetch at the feature/page level, not deep in the component tree
36+
- Transform data at the API boundary, not in components
37+
- Handle all three states: loading, error, and success
38+
39+
```typescript
40+
function ProjectPage({ projectId }: { projectId: string }) {
41+
const { data, error, isLoading } = useProjectData(projectId)
42+
43+
if (isLoading) {
44+
return <Skeleton />
45+
}
46+
47+
if (error) {
48+
return <ErrorState error={error} />
49+
}
50+
51+
return <ProjectView project={data} />
52+
}
53+
```
54+
55+
## Debouncing and Throttling
56+
57+
Debounce user input that triggers expensive operations. Keep references stable.
58+
59+
```typescript
60+
function useSearchTasks() {
61+
const [query, setQuery] = useState('')
62+
63+
const debouncedSearch = useMemo(
64+
() => debounce((value: string) => dispatch(searchTasks(value)), 300),
65+
[dispatch],
66+
)
67+
68+
const handleChange = useCallback(
69+
(event: React.ChangeEvent<HTMLInputElement>) => {
70+
const value = event.target.value
71+
setQuery(value)
72+
debouncedSearch(value)
73+
},
74+
[debouncedSearch],
75+
)
76+
77+
// Clean up on unmount
78+
useEffect(
79+
function cleanupDebouncedSearch() {
80+
return function cancelPendingSearch() {
81+
debouncedSearch.cancel()
82+
}
83+
},
84+
[debouncedSearch],
85+
)
86+
87+
return { query, handleChange }
88+
}
89+
```
90+
91+
## Rules
92+
93+
- **Always cancel stale operations** - Use `AbortController` in effects that fetch data
94+
- **Debounce user input** - Prevent excessive API calls from rapid typing or interaction
95+
- **Clean up on unmount** - Cancel timers, abort requests, unsubscribe from listeners
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# React Conventions
2+
3+
## JSX Conventions
4+
5+
- **Functional components only** - No class components
6+
- **Self-closing tags** for elements without children: `<Input />`
7+
- **Fragments** over wrapper divs: `<>...</>`
8+
- **Ternary over `&&`** for conditional rendering (avoids rendering `0`):
9+
10+
```typescript
11+
// Good: ternary
12+
{hasItems ? <ItemList items={items} /> : null}
13+
14+
// Bad: && can render falsy values
15+
{items.length && <ItemList items={items} />}
16+
```
17+
18+
- **Multiline JSX in parens**:
19+
20+
```typescript
21+
return (
22+
<div className={styles.container}>
23+
<Header />
24+
<Content />
25+
</div>
26+
)
27+
```
28+
29+
## Named Effect Callbacks
30+
31+
All `useEffect`, `useLayoutEffect`, and similar hook callbacks must be named functions. This improves stack traces and makes the effect's purpose clear.
32+
33+
```typescript
34+
// Good: named function
35+
useEffect(
36+
function fetchProjectData() {
37+
loadProject(projectId)
38+
},
39+
[projectId],
40+
)
41+
42+
// Good: named cleanup
43+
useEffect(
44+
function subscribeToUpdates() {
45+
const unsubscribe = subscribe(channelId)
46+
47+
return function unsubscribeFromUpdates() {
48+
unsubscribe()
49+
}
50+
},
51+
[channelId],
52+
)
53+
54+
// Bad: anonymous
55+
useEffect(() => {
56+
loadProject(projectId)
57+
}, [projectId])
58+
```
59+
60+
## Component Types
61+
62+
Define props as a `type` (named `Props` or with a descriptive prefix). Type children, event handlers, and refs explicitly.
63+
64+
```typescript
65+
type TaskItemProps = {
66+
task: Task
67+
onComplete: (id: string) => void
68+
onDelete: (id: string) => void
69+
}
70+
71+
function TaskItem({ task, onComplete, onDelete }: TaskItemProps) {
72+
// ...
73+
}
74+
75+
// Children typing
76+
type LayoutProps = {
77+
children: React.ReactNode
78+
sidebar?: React.ReactNode
79+
}
80+
81+
// Event handler typing
82+
type SearchProps = {
83+
onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
84+
}
85+
86+
// ForwardRef
87+
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
88+
return <input ref={ref} {...props} />
89+
})
90+
```

0 commit comments

Comments
 (0)