Add NDK skill documentation and examples
- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set.
This commit is contained in:
878
.claude/skills/react/examples/practical-patterns.tsx
Normal file
878
.claude/skills/react/examples/practical-patterns.tsx
Normal file
@@ -0,0 +1,878 @@
|
||||
# React Practical Examples
|
||||
|
||||
This file contains real-world examples of React patterns and solutions.
|
||||
|
||||
## Example 1: Custom Hook for Data Fetching
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface FetchState<T> {
|
||||
data: T | null
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
const useFetch = <T,>(url: string) => {
|
||||
const [state, setState] = useState<FetchState<T>>({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const controller = new AbortController()
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!cancelled) {
|
||||
setState({ data, loading: false, error: null })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled && error.name !== 'AbortError') {
|
||||
setState({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
controller.abort()
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// Usage
|
||||
const UserProfile = ({ userId }: { userId: string }) => {
|
||||
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`)
|
||||
|
||||
if (loading) return <Spinner />
|
||||
if (error) return <ErrorMessage error={error} />
|
||||
if (!data) return null
|
||||
|
||||
return <UserCard user={data} />
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Form with Validation
|
||||
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
age: z.number().min(18, 'Must be 18 or older')
|
||||
})
|
||||
|
||||
type UserForm = z.infer<typeof userSchema>
|
||||
type FormErrors = Partial<Record<keyof UserForm, string>>
|
||||
|
||||
const UserForm = () => {
|
||||
const [formData, setFormData] = useState<UserForm>({
|
||||
name: '',
|
||||
email: '',
|
||||
age: 0
|
||||
})
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleChange = useCallback((
|
||||
field: keyof UserForm,
|
||||
value: string | number
|
||||
) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
// Clear error when user starts typing
|
||||
setErrors(prev => ({ ...prev, [field]: undefined }))
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Validate
|
||||
const result = userSchema.safeParse(formData)
|
||||
if (!result.success) {
|
||||
const fieldErrors: FormErrors = {}
|
||||
result.error.errors.forEach(err => {
|
||||
const field = err.path[0] as keyof UserForm
|
||||
fieldErrors[field] = err.message
|
||||
})
|
||||
setErrors(fieldErrors)
|
||||
return
|
||||
}
|
||||
|
||||
// Submit
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await submitUser(result.data)
|
||||
// Success handling
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <span className="error">{errors.name}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={e => handleChange('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <span className="error">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="age">Age</label>
|
||||
<input
|
||||
id="age"
|
||||
type="number"
|
||||
value={formData.age || ''}
|
||||
onChange={e => handleChange('age', Number(e.target.value))}
|
||||
/>
|
||||
{errors.age && <span className="error">{errors.age}</span>}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 3: Modal with Portal
|
||||
|
||||
```typescript
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
children: React.ReactNode
|
||||
title?: string
|
||||
}
|
||||
|
||||
const Modal = ({ isOpen, onClose, children, title }: ModalProps) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
// Prevent body scroll
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [isOpen, onClose])
|
||||
|
||||
// Close on backdrop click
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === modalRef.current) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
{title && <h2 className="text-xl font-bold">{title}</h2>}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const App = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="My Modal">
|
||||
<p>Modal content goes here</p>
|
||||
<button onClick={() => setIsOpen(false)}>Close</button>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 4: Infinite Scroll
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
interface InfiniteScrollProps<T> {
|
||||
fetchData: (page: number) => Promise<T[]>
|
||||
renderItem: (item: T, index: number) => React.ReactNode
|
||||
loader?: React.ReactNode
|
||||
endMessage?: React.ReactNode
|
||||
}
|
||||
|
||||
const InfiniteScroll = <T extends { id: string | number },>({
|
||||
fetchData,
|
||||
renderItem,
|
||||
loader = <div>Loading...</div>,
|
||||
endMessage = <div>No more items</div>
|
||||
}: InfiniteScrollProps<T>) => {
|
||||
const [items, setItems] = useState<T[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const newItems = await fetchData(page)
|
||||
|
||||
if (newItems.length === 0) {
|
||||
setHasMore(false)
|
||||
} else {
|
||||
setItems(prev => [...prev, ...newItems])
|
||||
setPage(prev => prev + 1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load items:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, loading, hasMore, fetchData])
|
||||
|
||||
// Set up intersection observer
|
||||
useEffect(() => {
|
||||
observerRef.current = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
const currentRef = loadMoreRef.current
|
||||
if (currentRef) {
|
||||
observerRef.current.observe(currentRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observerRef.current && currentRef) {
|
||||
observerRef.current.unobserve(currentRef)
|
||||
}
|
||||
}
|
||||
}, [loadMore])
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadMore()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div ref={loadMoreRef}>
|
||||
{loading && loader}
|
||||
{!loading && !hasMore && endMessage}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const PostsList = () => {
|
||||
const fetchPosts = async (page: number) => {
|
||||
const response = await fetch(`/api/posts?page=${page}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll<Post>
|
||||
fetchData={fetchPosts}
|
||||
renderItem={(post) => <PostCard post={post} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 5: Dark Mode Toggle
|
||||
|
||||
```typescript
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | null>(null)
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within ThemeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
// Check localStorage and system preference
|
||||
const saved = localStorage.getItem('theme') as Theme | null
|
||||
if (saved) return saved
|
||||
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark'
|
||||
}
|
||||
|
||||
return 'light'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Update DOM and localStorage
|
||||
const root = document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(theme)
|
||||
localStorage.setItem('theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const ThemeToggle = () => {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<button onClick={toggleTheme} aria-label="Toggle theme">
|
||||
{theme === 'light' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 6: Debounced Search
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
|
||||
const useDebounce = <T,>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
const SearchPage = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<Product[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const debouncedQuery = useDebounce(query, 500)
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedQuery) {
|
||||
setResults([])
|
||||
return
|
||||
}
|
||||
|
||||
const searchProducts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(`/api/search?q=${debouncedQuery}`)
|
||||
const data = await response.json()
|
||||
setResults(data)
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
searchProducts()
|
||||
}, [debouncedQuery])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search products..."
|
||||
/>
|
||||
|
||||
{loading && <Spinner />}
|
||||
|
||||
{!loading && results.length > 0 && (
|
||||
<div>
|
||||
{results.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && query && results.length === 0 && (
|
||||
<p>No results found for "{query}"</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 7: Tabs Component
|
||||
|
||||
```typescript
|
||||
import { createContext, useContext, useState, useId } from 'react'
|
||||
|
||||
interface TabsContextType {
|
||||
activeTab: string
|
||||
setActiveTab: (id: string) => void
|
||||
tabsId: string
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextType | null>(null)
|
||||
|
||||
const useTabs = () => {
|
||||
const context = useContext(TabsContext)
|
||||
if (!context) throw new Error('Tabs compound components must be used within Tabs')
|
||||
return context
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
children: React.ReactNode
|
||||
defaultValue: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Tabs = ({ children, defaultValue, className }: TabsProps) => {
|
||||
const [activeTab, setActiveTab] = useState(defaultValue)
|
||||
const tabsId = useId()
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab, tabsId }}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsList = ({ children, className }: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
<div role="tablist" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface TabsTriggerProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabsTrigger = ({ value, children, className }: TabsTriggerProps) => {
|
||||
const { activeTab, setActiveTab, tabsId } = useTabs()
|
||||
const isActive = activeTab === value
|
||||
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
id={`${tabsId}-tab-${value}`}
|
||||
aria-controls={`${tabsId}-panel-${value}`}
|
||||
aria-selected={isActive}
|
||||
onClick={() => setActiveTab(value)}
|
||||
className={`${className} ${isActive ? 'active' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface TabsContentProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabsContent = ({ value, children, className }: TabsContentProps) => {
|
||||
const { activeTab, tabsId } = useTabs()
|
||||
|
||||
if (activeTab !== value) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`${tabsId}-panel-${value}`}
|
||||
aria-labelledby={`${tabsId}-tab-${value}`}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Export compound component
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
|
||||
// Usage
|
||||
const App = () => (
|
||||
<Tabs defaultValue="profile">
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile">
|
||||
<h2>Profile Content</h2>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
<h2>Settings Content</h2>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<h2>Notifications Content</h2>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
```
|
||||
|
||||
## Example 8: Error Boundary
|
||||
|
||||
```typescript
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: (error: Error, reset: () => void) => ReactNode
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
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('ErrorBoundary caught:', error, errorInfo)
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.setState({ hasError: false, error: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback(this.state.error, this.reset)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="error-boundary">
|
||||
<h2>Something went wrong</h2>
|
||||
<details>
|
||||
<summary>Error details</summary>
|
||||
<pre>{this.state.error.message}</pre>
|
||||
</details>
|
||||
<button onClick={this.reset}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const App = () => (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => (
|
||||
<div>
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p>{error.message}</p>
|
||||
<button onClick={reset}>Retry</button>
|
||||
</div>
|
||||
)}
|
||||
onError={(error, errorInfo) => {
|
||||
// Send to error tracking service
|
||||
console.error('Error logged:', error, errorInfo)
|
||||
}}
|
||||
>
|
||||
<YourApp />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
```
|
||||
|
||||
## Example 9: Custom Hook for Local Storage
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
const useLocalStorage = <T,>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T | ((val: T) => T)) => void, () => void] => {
|
||||
// Get initial value from localStorage
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
console.error(`Error loading ${key} from localStorage:`, error)
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
|
||||
// Update localStorage when value changes
|
||||
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
|
||||
// Dispatch storage event for other tabs
|
||||
window.dispatchEvent(new Event('storage'))
|
||||
} catch (error) {
|
||||
console.error(`Error saving ${key} to localStorage:`, error)
|
||||
}
|
||||
}, [key, storedValue])
|
||||
|
||||
// Remove from localStorage
|
||||
const removeValue = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
setStoredValue(initialValue)
|
||||
} catch (error) {
|
||||
console.error(`Error removing ${key} from localStorage:`, error)
|
||||
}
|
||||
}, [key, initialValue])
|
||||
|
||||
// Listen for changes in other tabs
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue) {
|
||||
setStoredValue(JSON.parse(e.newValue))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
return () => window.removeEventListener('storage', handleStorageChange)
|
||||
}, [key])
|
||||
|
||||
return [storedValue, setValue, removeValue]
|
||||
}
|
||||
|
||||
// Usage
|
||||
const UserPreferences = () => {
|
||||
const [preferences, setPreferences, clearPreferences] = useLocalStorage('user-prefs', {
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
notifications: true
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={preferences.notifications}
|
||||
onChange={e => setPreferences({
|
||||
...preferences,
|
||||
notifications: e.target.checked
|
||||
})}
|
||||
/>
|
||||
Enable notifications
|
||||
</label>
|
||||
|
||||
<button onClick={clearPreferences}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 10: Optimistic Updates with useOptimistic
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useOptimistic } from 'react'
|
||||
import { likePost, unlikePost } from './actions'
|
||||
|
||||
interface Post {
|
||||
id: string
|
||||
content: string
|
||||
likes: number
|
||||
isLiked: boolean
|
||||
}
|
||||
|
||||
const PostCard = ({ post }: { post: Post }) => {
|
||||
const [optimisticPost, addOptimistic] = useOptimistic(
|
||||
post,
|
||||
(currentPost, update: Partial<Post>) => ({
|
||||
...currentPost,
|
||||
...update
|
||||
})
|
||||
)
|
||||
|
||||
const handleLike = async () => {
|
||||
// Optimistically update UI
|
||||
addOptimistic({
|
||||
likes: optimisticPost.likes + 1,
|
||||
isLiked: true
|
||||
})
|
||||
|
||||
try {
|
||||
// Send server request
|
||||
await likePost(post.id)
|
||||
} catch (error) {
|
||||
// Server will send correct state via revalidation
|
||||
console.error('Failed to like post:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnlike = async () => {
|
||||
addOptimistic({
|
||||
likes: optimisticPost.likes - 1,
|
||||
isLiked: false
|
||||
})
|
||||
|
||||
try {
|
||||
await unlikePost(post.id)
|
||||
} catch (error) {
|
||||
console.error('Failed to unlike post:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post-card">
|
||||
<p>{optimisticPost.content}</p>
|
||||
<button
|
||||
onClick={optimisticPost.isLiked ? handleUnlike : handleLike}
|
||||
className={optimisticPost.isLiked ? 'liked' : ''}
|
||||
>
|
||||
❤️ {optimisticPost.likes}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
These examples demonstrate:
|
||||
- Custom hooks for reusable logic
|
||||
- Form handling with validation
|
||||
- Portal usage for modals
|
||||
- Infinite scroll with Intersection Observer
|
||||
- Context for global state
|
||||
- Debouncing for performance
|
||||
- Compound components pattern
|
||||
- Error boundaries
|
||||
- LocalStorage integration
|
||||
- Optimistic updates (React 19)
|
||||
|
||||
Reference in New Issue
Block a user