Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions docs/react-guidelines/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# React Async Patterns

## Race Conditions

Use `AbortController` and `useEffect` cleanup to cancel stale operations.

```typescript
useEffect(
function fetchProjectData() {
const controller = new AbortController()

api.getProject(projectId, { signal: controller.signal })
.then((data) => {
setProject(data)
})
.catch((error) => {
if (isAbortError(error?.cause ?? error)) {
return // expected: request was cancelled
}
setError(error)
})

return function abortFetchProjectData() {
controller.abort()
}
},
[projectId],
)
```

When a dependency changes before the previous request completes, the cleanup function aborts the stale request, preventing updates from out-of-order responses.

## Data Fetching

- Fetch at the feature/page level, not deep in the component tree
- Transform data at the API boundary, not in components
- Handle all three states: loading, error, and success

```typescript
function ProjectPage({ projectId }: { projectId: string }) {
const { data, error, isLoading } = useProjectData(projectId)

if (isLoading) {
return <Skeleton />
}

if (error) {
return <ErrorState error={error} />
}

return <ProjectView project={data} />
}
```

## Debouncing and Throttling

Debounce user input that triggers expensive operations. Keep references stable.

```typescript
function useSearchTasks() {
const [query, setQuery] = useState('')

const debouncedSearch = useMemo(
() => debounce((value: string) => dispatch(searchTasks(value)), 300),
[dispatch],
)

const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
setQuery(value)
debouncedSearch(value)
},
[debouncedSearch],
)

// Clean up on unmount
useEffect(
function cleanupDebouncedSearch() {
return function cancelPendingSearch() {
debouncedSearch.cancel()
}
},
[debouncedSearch],
)

return { query, handleChange }
}
```

## Rules

- **Always cancel stale operations** - Use `AbortController` in effects that fetch data
- **Debounce user input** - Prevent excessive API calls from rapid typing or interaction
- **Clean up on unmount** - Cancel timers, abort requests, unsubscribe from listeners
90 changes: 90 additions & 0 deletions docs/react-guidelines/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# React Conventions

## JSX Conventions

- **Functional components only** - No class components
- **Self-closing tags** for elements without children: `<Input />`
- **Fragments** over wrapper divs: `<>...</>`
- **Ternary over `&&`** for conditional rendering (avoids rendering `0`):

```typescript
// Good: ternary
{hasItems ? <ItemList items={items} /> : null}

// Bad: && can render falsy values
{items.length && <ItemList items={items} />}
```

- **Multiline JSX in parens**:

```typescript
return (
<div className={styles.container}>
<Header />
<Content />
</div>
)
```

## Named Effect Callbacks

All `useEffect`, `useLayoutEffect`, and similar hook callbacks must be named functions. This improves stack traces and makes the effect's purpose clear.

```typescript
// Good: named function
useEffect(
function fetchProjectData() {
loadProject(projectId)
},
[projectId],
)

// Good: named cleanup
useEffect(
function subscribeToUpdates() {
const unsubscribe = subscribe(channelId)

return function unsubscribeFromUpdates() {
unsubscribe()
}
},
[channelId],
)

// Bad: anonymous
useEffect(() => {
loadProject(projectId)
}, [projectId])
```

## Component Types

Define props as a `type` (named `Props` or with a descriptive prefix). Type children, event handlers, and refs explicitly.

```typescript
type TaskItemProps = {
task: Task
onComplete: (id: string) => void
onDelete: (id: string) => void
}

function TaskItem({ task, onComplete, onDelete }: TaskItemProps) {
// ...
}

// Children typing
type LayoutProps = {
children: React.ReactNode
sidebar?: React.ReactNode
}

// Event handler typing
type SearchProps = {
onSearch: (event: React.ChangeEvent<HTMLInputElement>) => void
}

// ForwardRef
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />
})
```
Loading