React Development
Installation
SKILL.md
React Development
React is a JavaScript library for building user interfaces using components.
Core Concepts
Component Types
Function Components (Recommended):
// Basic component
interface ButtonProps {
children: React.ReactNode
onClick?: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
}
export function Button({
children,
onClick,
variant = 'primary',
disabled = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{children}
</button>
)
}
// Component with forwardRef
import { forwardRef } from 'react'
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string
error?: string
}
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="form-field">
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
)
}
)
Input.displayName = 'Input'
// Generic component
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string | number
}
export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
)
}
// Usage
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <UserCard user={user} />}
/>
Hooks
useState:
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const [user, setUser] = useState<User | null>(null)
// Functional update
const increment = () => setCount((prev) => prev + 1)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
// With lazy initialization
function ExpensiveComponent() {
const [data, setData] = useState(() => computeExpensiveValue())
// ...
}
useEffect:
import { useEffect, useState } from 'react'
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
// Fetch on mount and when userId changes
useEffect(() => {
let cancelled = false
async function fetchUser() {
setLoading(true)
try {
const data = await api.getUser(userId)
if (!cancelled) {
setUser(data)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup
return () => {
cancelled = true
}
}, [userId])
// Subscribe to event
useEffect(() => {
function handleResize() {
console.log('Resized')
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [])
if (loading) return <Loading />
if (!user) return <NotFound />
return <UserCard user={user} />
}
useContext:
import { createContext, useContext, useState } from 'react'
interface AuthContextValue {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isLoading: boolean
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(false)
const login = async (email: string, password: string) => {
setIsLoading(true)
try {
const user = await api.login(email, password)
setUser(user)
} finally {
setIsLoading(false)
}
}
const logout = () => {
api.logout()
setUser(null)
}
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}
// Custom hook for using context
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Usage
function Header() {
const { user, logout } = useAuth()
return (
<header>
{user ? (
<>
<span>Welcome, {user.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</header>
)
}
useReducer:
import { useReducer } from 'react'
interface State {
count: number
step: number
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'setStep'; payload: number }
| { type: 'reset' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step }
case 'decrement':
return { ...state, count: state.count - state.step }
case 'setStep':
return { ...state, step: action.payload }
case 'reset':
return { count: 0, step: 1 }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 })
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({ type: 'setStep', payload: Number(e.target.value) })
}
/>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
)
}
useMemo and useCallback:
import { useMemo, useCallback, useState } from 'react'
function DataTable({ data, filter }: { data: Item[]; filter: string }) {
const [sortKey, setSortKey] = useState<keyof Item>('name')
// Memoize expensive computation
const filteredAndSortedData = useMemo(() => {
return data
.filter((item) => item.name.includes(filter))
.sort((a, b) => String(a[sortKey]).localeCompare(String(b[sortKey])))
}, [data, filter, sortKey])
// Memoize callback
const handleSort = useCallback((key: keyof Item) => {
setSortKey(key)
}, [])
return (
<table>
<thead>
<tr>
<SortableHeader sortKey="name" onSort={handleSort}>
Name
</SortableHeader>
<SortableHeader sortKey="value" onSort={handleSort}>
Value
</SortableHeader>
</tr>
</thead>
<tbody>
{filteredAndSortedData.map((item) => (
<Row key={item.id} item={item} />
))}
</tbody>
</table>
)
}
Custom Hooks:
// useLocalStorage.ts
import { useState, useEffect, useCallback } from 'react'
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? (JSON.parse(item) as T) : initialValue
} catch (error) {
console.error(error)
return initialValue
}
})
const setValue = useCallback(
(value: T | ((prev: 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)
}
},
[key, storedValue]
)
return [storedValue, setValue]
}
// useFetch.ts
import { useState, useEffect } from 'react'
interface UseFetchResult<T> {
data: T | null
error: Error | null
isLoading: boolean
}
export function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const controller = new AbortController()
async function fetchData() {
try {
setIsLoading(true)
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const json = await response.json()
setData(json)
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err)
}
} finally {
setIsLoading(false)
}
}
fetchData()
return () => controller.abort()
}, [url])
return { data, error, isLoading }
}
// Usage
function UserProfile({ userId }: { userId: string }) {
const { data: user, error, isLoading } = useFetch<User>(`/api/users/${userId}`)
if (isLoading) return <Loading />
if (error) return <Error message={error.message} />
if (!user) return <NotFound />
return <UserCard user={user} />
}
Patterns
Compound Components:
import { createContext, useContext, useState } from 'react'
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextValue | null>(null)
function useTabs() {
const context = useContext(TabsContext)
if (!context) throw new Error('Must be used within Tabs')
return context
}
// Root component
export function Tabs({
children,
defaultTab,
}: {
children: React.ReactNode
defaultTab: string
}) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
// List component
export function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>
}
// Trigger component
export function TabTrigger({
value,
children,
}: {
value: string
children: React.ReactNode
}) {
const { activeTab, setActiveTab } = useTabs()
return (
<button
className={activeTab === value ? 'active' : ''}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
// Content component
export function TabContent({
value,
children,
}: {
value: string
children: React.ReactNode
}) {
const { activeTab } = useTabs()
if (activeTab !== value) return null
return <div className="tab-content">{children}</div>
}
// Usage
<Tabs defaultTab="account">
<TabList>
<TabTrigger value="account">Account</TabTrigger>
<TabTrigger value="password">Password</TabTrigger>
<TabTrigger value="notifications">Notifications</TabTrigger>
</TabList>
<TabContent value="account">
<AccountSettings />
</TabContent>
<TabContent value="password">
<PasswordSettings />
</TabContent>
<TabContent value="notifications">
<NotificationSettings />
</TabContent>
</Tabs>
Render Props:
interface ToggleProps {
children: (props: { on: boolean; toggle: () => void }) => React.ReactNode
initialOn?: boolean
}
function Toggle({ children, initialOn = false }: ToggleProps) {
const [on, setOn] = useState(initialOn)
const toggle = () => setOn((prev) => !prev)
return <>{children({ on, toggle })}</>
}
// Usage
<Toggle>
{({ on, toggle }) => (
<button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>
)}
</Toggle>
Higher-Order Components (HOCs):
import { ComponentType } from 'react'
interface WithLoadingProps {
isLoading: boolean
}
function withLoading<P extends object>(
WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
return function WithLoadingComponent({
isLoading,
...props
}: P & WithLoadingProps) {
if (isLoading) return <LoadingSpinner />
return <WrappedComponent {...(props as P)} />
}
}
// Usage
const UserProfileWithLoading = withLoading(UserProfile)
Performance Optimization
React.memo:
import { memo } from 'react'
interface ExpensiveComponentProps {
data: DataItem[]
onItemClick: (id: string) => void
}
const ExpensiveComponent = memo<ExpensiveComponentProps>(
({ data, onItemClick }) => {
return (
<ul>
{data.map((item) => (
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
)
},
// Custom comparison
(prevProps, nextProps) => {
return prevProps.data.length === nextProps.data.length
}
)
ExpensiveComponent.displayName = 'ExpensiveComponent'
Code Splitting:
import { lazy, Suspense } from 'react'
// Lazy load components
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
Best Practices
- Use TypeScript - Catch errors at compile time
- Keep components small - Single responsibility
- Lift state carefully - Avoid prop drilling
- Use custom hooks - Extract reusable logic
- Memoize when needed - But don't premature optimize
- Handle loading/error states - Good UX
- Clean up side effects - Prevent memory leaks
Additional Resources
references/react-patterns.md- Advanced patternsexamples/react-components/- Component examples