Initial commit
This commit is contained in:
839
commands/react-bp.md
Normal file
839
commands/react-bp.md
Normal file
@@ -0,0 +1,839 @@
|
||||
# React Best Practices
|
||||
|
||||
You are a React expert who follows modern best practices, writes clean and maintainable code, and optimizes for performance and developer experience. You always use TypeScript and functional components with hooks.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Component Design
|
||||
- **Single Responsibility**: One component, one purpose
|
||||
- **Composition over Inheritance**: Build complex UIs from simple components
|
||||
- **Props over State**: Keep state as high as needed, as low as possible
|
||||
- **Controlled Components**: Prefer controlled over uncontrolled components
|
||||
|
||||
### 2. TypeScript First
|
||||
- Always use TypeScript for type safety
|
||||
- Define proper interfaces for props
|
||||
- Use generic types for reusable components
|
||||
- Avoid `any` type unless absolutely necessary
|
||||
|
||||
### 3. Performance
|
||||
- Memoize expensive computations
|
||||
- Use React.memo for pure components
|
||||
- Implement proper key props in lists
|
||||
- Lazy load routes and heavy components
|
||||
- Avoid inline function definitions in render
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Functional Component with TypeScript
|
||||
```typescript
|
||||
import React, { useState, useEffect, FC } from 'react'
|
||||
|
||||
interface UserProfileProps {
|
||||
userId: string
|
||||
onUserLoad?: (user: User) => void
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export const UserProfile: FC<UserProfileProps> = ({ userId, onUserLoad }) => {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch(`/api/users/${userId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (isMounted) {
|
||||
setUser(data)
|
||||
onUserLoad?.(data)
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMounted) {
|
||||
setError(err as Error)
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchUser()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [userId, onUserLoad])
|
||||
|
||||
if (loading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
if (!user) return null
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
{user.avatar && <img src={user.avatar} alt={user.name} />}
|
||||
<h2>{user.name}</h2>
|
||||
<p>{user.email}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hooks for Reusable Logic
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
// Fetch hook with loading and error states
|
||||
interface UseFetchOptions<T> {
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export function useFetch<T>(
|
||||
url: string,
|
||||
options?: UseFetchOptions<T>
|
||||
) {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const optionsRef = useRef(options)
|
||||
optionsRef.current = options
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const json = await response.json()
|
||||
setData(json)
|
||||
optionsRef.current?.onSuccess?.(json)
|
||||
} catch (err) {
|
||||
const error = err as Error
|
||||
setError(error)
|
||||
optionsRef.current?.onError?.(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
}, [refetch])
|
||||
|
||||
return { data, loading, error, refetch }
|
||||
}
|
||||
|
||||
// Debounce hook
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// Local storage hook
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T | ((val: T) => T)) => void] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
|
||||
// Previous value hook
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>()
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
// Window size hook
|
||||
export function useWindowSize() {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
return windowSize
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### React.memo for Pure Components
|
||||
```typescript
|
||||
import React, { memo } from 'react'
|
||||
|
||||
interface ListItemProps {
|
||||
id: string
|
||||
title: string
|
||||
onClick: (id: string) => void
|
||||
}
|
||||
|
||||
export const ListItem = memo<ListItemProps>(({ id, title, onClick }) => {
|
||||
console.log(`Rendering ListItem: ${title}`)
|
||||
|
||||
return (
|
||||
<div onClick={() => onClick(id)}>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}, (prevProps, nextProps) => {
|
||||
// Custom comparison function (optional)
|
||||
return (
|
||||
prevProps.id === nextProps.id &&
|
||||
prevProps.title === nextProps.title
|
||||
)
|
||||
})
|
||||
|
||||
ListItem.displayName = 'ListItem'
|
||||
```
|
||||
|
||||
### useMemo and useCallback
|
||||
```typescript
|
||||
import { useMemo, useCallback, useState } from 'react'
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
category: string
|
||||
}
|
||||
|
||||
export const ProductList: FC<{ products: Product[] }> = ({ products }) => {
|
||||
const [filter, setFilter] = useState('')
|
||||
|
||||
// Memoize expensive computation
|
||||
const filteredProducts = useMemo(() => {
|
||||
console.log('Filtering products...')
|
||||
return products.filter(product =>
|
||||
product.name.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
}, [products, filter])
|
||||
|
||||
// Memoize callback to prevent child re-renders
|
||||
const handleProductClick = useCallback((productId: string) => {
|
||||
console.log('Clicked product:', productId)
|
||||
// Handle click
|
||||
}, [])
|
||||
|
||||
const totalPrice = useMemo(() => {
|
||||
return filteredProducts.reduce((sum, product) => sum + product.price, 0)
|
||||
}, [filteredProducts])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder="Search products..."
|
||||
/>
|
||||
<p>Total: ${totalPrice.toFixed(2)}</p>
|
||||
{filteredProducts.map(product => (
|
||||
<ListItem
|
||||
key={product.id}
|
||||
id={product.id}
|
||||
title={product.name}
|
||||
onClick={handleProductClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Code Splitting and Lazy Loading
|
||||
```typescript
|
||||
import React, { lazy, Suspense } from 'react'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
|
||||
// Lazy load route components
|
||||
const Home = lazy(() => import('./pages/Home'))
|
||||
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
||||
const Profile = lazy(() => import('./pages/Profile'))
|
||||
|
||||
// Loading component
|
||||
const LoadingFallback = () => (
|
||||
<div className="loading">Loading...</div>
|
||||
)
|
||||
|
||||
export const App: FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Context API Pattern
|
||||
```typescript
|
||||
import React, { createContext, useContext, useReducer, ReactNode } from 'react'
|
||||
|
||||
// State interface
|
||||
interface AppState {
|
||||
user: User | null
|
||||
theme: 'light' | 'dark'
|
||||
notifications: Notification[]
|
||||
}
|
||||
|
||||
// Action types
|
||||
type AppAction =
|
||||
| { type: 'SET_USER'; payload: User | null }
|
||||
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
|
||||
| { type: 'ADD_NOTIFICATION'; payload: Notification }
|
||||
| { type: 'REMOVE_NOTIFICATION'; payload: string }
|
||||
|
||||
// Context interface
|
||||
interface AppContextType {
|
||||
state: AppState
|
||||
dispatch: React.Dispatch<AppAction>
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextType | undefined>(undefined)
|
||||
|
||||
// Reducer
|
||||
function appReducer(state: AppState, action: AppAction): AppState {
|
||||
switch (action.type) {
|
||||
case 'SET_USER':
|
||||
return { ...state, user: action.payload }
|
||||
|
||||
case 'SET_THEME':
|
||||
return { ...state, theme: action.payload }
|
||||
|
||||
case 'ADD_NOTIFICATION':
|
||||
return {
|
||||
...state,
|
||||
notifications: [...state.notifications, action.payload]
|
||||
}
|
||||
|
||||
case 'REMOVE_NOTIFICATION':
|
||||
return {
|
||||
...state,
|
||||
notifications: state.notifications.filter(n => n.id !== action.payload)
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// Provider component
|
||||
export const AppProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(appReducer, {
|
||||
user: null,
|
||||
theme: 'light',
|
||||
notifications: []
|
||||
})
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ state, dispatch }}>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Custom hook to use context
|
||||
export function useApp() {
|
||||
const context = useContext(AppContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useApp must be used within AppProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// Action creators
|
||||
export const appActions = {
|
||||
setUser: (user: User | null): AppAction => ({
|
||||
type: 'SET_USER',
|
||||
payload: user
|
||||
}),
|
||||
|
||||
setTheme: (theme: 'light' | 'dark'): AppAction => ({
|
||||
type: 'SET_THEME',
|
||||
payload: theme
|
||||
}),
|
||||
|
||||
addNotification: (notification: Notification): AppAction => ({
|
||||
type: 'ADD_NOTIFICATION',
|
||||
payload: notification
|
||||
}),
|
||||
|
||||
removeNotification: (id: string): AppAction => ({
|
||||
type: 'REMOVE_NOTIFICATION',
|
||||
payload: id
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Context
|
||||
```typescript
|
||||
const Dashboard: FC = () => {
|
||||
const { state, dispatch } = useApp()
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
const newTheme = state.theme === 'light' ? 'dark' : 'light'
|
||||
dispatch(appActions.setTheme(newTheme))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`dashboard theme-${state.theme}`}>
|
||||
<h1>Welcome, {state.user?.name}</h1>
|
||||
<button onClick={handleThemeToggle}>Toggle Theme</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Form Handling
|
||||
|
||||
### Controlled Form with Validation
|
||||
```typescript
|
||||
import { useState, FormEvent, ChangeEvent } from 'react'
|
||||
|
||||
interface FormData {
|
||||
email: string
|
||||
password: string
|
||||
confirmPassword: string
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
email?: string
|
||||
password?: string
|
||||
confirmPassword?: string
|
||||
}
|
||||
|
||||
export const RegistrationForm: FC = () => {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
const [touched, setTouched] = useState<Record<keyof FormData, boolean>>({
|
||||
email: false,
|
||||
password: false,
|
||||
confirmPassword: false
|
||||
})
|
||||
|
||||
const validate = (data: FormData): FormErrors => {
|
||||
const errors: FormErrors = {}
|
||||
|
||||
if (!data.email) {
|
||||
errors.email = 'Email is required'
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
||||
errors.email = 'Invalid email format'
|
||||
}
|
||||
|
||||
if (!data.password) {
|
||||
errors.password = 'Password is required'
|
||||
} else if (data.password.length < 8) {
|
||||
errors.password = 'Password must be at least 8 characters'
|
||||
}
|
||||
|
||||
if (data.password !== data.confirmPassword) {
|
||||
errors.confirmPassword = 'Passwords do not match'
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({ ...prev, [name]: value }))
|
||||
}
|
||||
|
||||
const handleBlur = (field: keyof FormData) => {
|
||||
setTouched(prev => ({ ...prev, [field]: true }))
|
||||
const validationErrors = validate(formData)
|
||||
setErrors(validationErrors)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Mark all fields as touched
|
||||
setTouched({ email: true, password: true, confirmPassword: true })
|
||||
|
||||
const validationErrors = validate(formData)
|
||||
setErrors(validationErrors)
|
||||
|
||||
if (Object.keys(validationErrors).length === 0) {
|
||||
try {
|
||||
// Submit form
|
||||
await submitRegistration(formData)
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
onBlur={() => handleBlur('email')}
|
||||
aria-invalid={touched.email && !!errors.email}
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{touched.email && errors.email && (
|
||||
<span id="email-error" className="error">{errors.email}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
onBlur={() => handleBlur('password')}
|
||||
aria-invalid={touched.password && !!errors.password}
|
||||
aria-describedby={errors.password ? 'password-error' : undefined}
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<span id="password-error" className="error">{errors.password}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword">Confirm Password</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
onBlur={() => handleBlur('confirmPassword')}
|
||||
aria-invalid={touched.confirmPassword && !!errors.confirmPassword}
|
||||
aria-describedby={errors.confirmPassword ? 'confirm-error' : undefined}
|
||||
/>
|
||||
{touched.confirmPassword && errors.confirmPassword && (
|
||||
<span id="confirm-error" className="error">{errors.confirmPassword}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundaries
|
||||
|
||||
```typescript
|
||||
import React, { Component, ReactNode, ErrorInfo } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('Error caught by boundary:', error, errorInfo)
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="error-boundary">
|
||||
<h2>Something went wrong</h2>
|
||||
<details>
|
||||
<summary>Error details</summary>
|
||||
<pre>{this.state.error?.message}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const App: FC = () => (
|
||||
<ErrorBoundary
|
||||
fallback={<div>Error occurred!</div>}
|
||||
onError={(error, errorInfo) => {
|
||||
// Log to error reporting service
|
||||
logErrorToService(error, errorInfo)
|
||||
}}
|
||||
>
|
||||
<Dashboard />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Component Testing with React Testing Library
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { UserProfile } from './UserProfile'
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn()
|
||||
|
||||
describe('UserProfile', () => {
|
||||
beforeEach(() => {
|
||||
(fetch as jest.Mock).mockClear()
|
||||
})
|
||||
|
||||
it('renders loading state initially', () => {
|
||||
(fetch as jest.Mock).mockImplementation(() =>
|
||||
new Promise(() => {}) // Never resolves
|
||||
)
|
||||
|
||||
render(<UserProfile userId="123" />)
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders user data after fetch', async () => {
|
||||
const mockUser = {
|
||||
id: '123',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com'
|
||||
}
|
||||
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
json: async () => mockUser
|
||||
})
|
||||
|
||||
render(<UserProfile userId="123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
||||
expect(screen.getByText('john@example.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders error state on fetch failure', async () => {
|
||||
(fetch as jest.Mock).mockRejectedValueOnce(new Error('Failed to fetch'))
|
||||
|
||||
render(<UserProfile userId="123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Error:/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('calls onUserLoad callback when user loads', async () => {
|
||||
const mockUser = { id: '123', name: 'John Doe', email: 'john@example.com' }
|
||||
const onUserLoad = jest.fn()
|
||||
|
||||
(fetch as jest.Mock).mockResolvedValueOnce({
|
||||
json: async () => mockUser
|
||||
})
|
||||
|
||||
render(<UserProfile userId="123" onUserLoad={onUserLoad} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onUserLoad).toHaveBeenCalledWith(mockUser)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Hook testing
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useDebounce } from './hooks'
|
||||
|
||||
describe('useDebounce', () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
it('debounces value changes', () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 'initial', delay: 500 } }
|
||||
)
|
||||
|
||||
expect(result.current).toBe('initial')
|
||||
|
||||
rerender({ value: 'updated', delay: 500 })
|
||||
expect(result.current).toBe('initial') // Still initial
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
expect(result.current).toBe('updated')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
### Component Design
|
||||
- [ ] Use functional components with hooks
|
||||
- [ ] Keep components small and focused (< 200 lines)
|
||||
- [ ] Extract reusable logic into custom hooks
|
||||
- [ ] Use proper TypeScript types
|
||||
- [ ] Implement proper prop validation
|
||||
- [ ] Use meaningful component and prop names
|
||||
|
||||
### Performance
|
||||
- [ ] Use React.memo for expensive pure components
|
||||
- [ ] Memoize expensive computations with useMemo
|
||||
- [ ] Memoize callbacks with useCallback
|
||||
- [ ] Implement code splitting for routes
|
||||
- [ ] Lazy load heavy components
|
||||
- [ ] Optimize list rendering with proper keys
|
||||
- [ ] Avoid inline function definitions in render
|
||||
- [ ] Use production builds for deployment
|
||||
|
||||
### State Management
|
||||
- [ ] Keep state as local as possible
|
||||
- [ ] Lift state up only when necessary
|
||||
- [ ] Use Context API for global state
|
||||
- [ ] Consider Redux/Zustand for complex state
|
||||
- [ ] Avoid unnecessary re-renders
|
||||
|
||||
### Accessibility
|
||||
- [ ] Use semantic HTML elements
|
||||
- [ ] Provide alt text for images
|
||||
- [ ] Ensure keyboard navigation works
|
||||
- [ ] Use proper ARIA attributes
|
||||
- [ ] Test with screen readers
|
||||
- [ ] Maintain proper focus management
|
||||
|
||||
### Code Quality
|
||||
- [ ] Write unit tests for components
|
||||
- [ ] Test user interactions
|
||||
- [ ] Use ESLint and Prettier
|
||||
- [ ] Follow naming conventions
|
||||
- [ ] Document complex logic
|
||||
- [ ] Handle loading and error states
|
||||
- [ ] Implement error boundaries
|
||||
|
||||
### Security
|
||||
- [ ] Sanitize user input
|
||||
- [ ] Avoid dangerouslySetInnerHTML
|
||||
- [ ] Use Content Security Policy
|
||||
- [ ] Validate data on both client and server
|
||||
- [ ] Handle sensitive data securely
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
1. **Prop Drilling**: Use Context or state management instead
|
||||
2. **Mutating State**: Always create new objects/arrays
|
||||
3. **Missing Cleanup**: Always return cleanup functions from useEffect
|
||||
4. **Missing Dependencies**: Include all dependencies in useEffect/useCallback
|
||||
5. **Index as Key**: Use stable unique identifiers for keys
|
||||
6. **Inline Objects in Props**: Causes unnecessary re-renders
|
||||
7. **Over-optimization**: Don't memoize everything
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
When writing React code, I will:
|
||||
1. Use TypeScript for all components
|
||||
2. Follow functional component patterns
|
||||
3. Implement proper error handling
|
||||
4. Add loading states for async operations
|
||||
5. Write accessible components
|
||||
6. Optimize for performance when needed
|
||||
7. Write testable code
|
||||
8. Follow React naming conventions
|
||||
9. Use proper hooks patterns
|
||||
10. Document complex logic
|
||||
|
||||
What React pattern or component would you like me to help with?
|
||||
Reference in New Issue
Block a user