Help us improve
Share bugs, ideas, or general feedback.
From shipshape-skills
Provides React patterns for building components with props and composition, state management via Hooks/Zustand/Context, custom hooks, data fetching, performance optimization, and compound components like Tabs.
npx claudepluginhub mukiwu/muki-ai-plugins --plugin shipshape-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/shipshape-skills:react-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Modern frontend patterns for React with Hooks and TypeScript.
Guides implementation of modern React patterns: hooks, component composition, state management, performance optimizations, concurrent features. Use for building or refactoring components.
Provides React patterns like compound components, render props, custom hooks, and HOCs for scalable frontend apps. Use for component design, state management, and best practices.
Provides 10 high-impact React patterns and anti-patterns for state management, performance, hooks, and component design. Use when writing or reviewing React components.
Share bugs, ideas, or general feedback.
Modern frontend patterns for React with Hooks and TypeScript.
interface UserCardProps {
user: User
variant?: 'default' | 'outlined'
onSelect: (id: string) => void
}
function UserCard({ user, variant = 'default', onSelect }: UserCardProps) {
return (
<div className={`card card-${variant}`}>
<span>{user.name}</span>
<button onClick={() => onSelect(user.id)}>Select</button>
</div>
)
}
function Card({ children, variant = 'default' }: CardProps) {
return <div className={`card card-${variant}`}>{children}</div>
}
function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>
}
function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>
const TabsContext = createContext<{
activeTab: string
setActiveTab: (tab: string) => void
} | undefined>(undefined)
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const context = useContext(TabsContext)
if (!context) throw new Error('Tab must be used within Tabs')
return (
<button
className={context.activeTab === id ? 'active' : ''}
onClick={() => context.setActiveTab(id)}
>
{children}
</button>
)
}
export function useQuery<T>(key: string, fetcher: () => Promise<T>, options?: {
onSuccess?: (data: T) => void
onError?: (error: Error) => void
enabled?: boolean
}) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(false)
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetcher()
setData(result)
options?.onSuccess?.(result)
} catch (err) {
const error = err as Error
setError(error)
options?.onError?.(error)
} finally {
setLoading(false)
}
}, [fetcher, options])
useEffect(() => {
if (options?.enabled !== false) refetch()
}, [key])
return { data, error, loading, refetch }
}
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 500)
export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle]
}
interface State {
items: Item[]
selected: Item | null
loading: boolean
}
type Action =
| { type: 'SET_ITEMS'; payload: Item[] }
| { type: 'SELECT'; payload: Item }
| { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_ITEMS':
return { ...state, items: action.payload }
case 'SELECT':
return { ...state, selected: action.payload }
case 'SET_LOADING':
return { ...state, loading: action.payload }
}
}
const ItemContext = createContext<{
state: State
dispatch: Dispatch<Action>
} | undefined>(undefined)
export function ItemProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
items: [],
selected: null,
loading: false
})
return (
<ItemContext.Provider value={{ state, dispatch }}>
{children}
</ItemContext.Provider>
)
}
export function useItems() {
const context = useContext(ItemContext)
if (!context) throw new Error('useItems must be used within ItemProvider')
return context
}
import { create } from 'zustand'
interface UserStore {
users: User[]
loading: boolean
fetchUsers: () => Promise<void>
}
export const useUserStore = create<UserStore>((set) => ({
users: [],
loading: false,
fetchUsers: async () => {
set({ loading: true })
try {
const users = await api.getUsers()
set({ users })
} finally {
set({ loading: false })
}
}
}))
// useMemo — 昂貴計算快取
const sortedList = useMemo(() => [...items].sort((a, b) => b.score - a.score), [items])
// useCallback — 傳給子元件的函式穩定參考
const handleClick = useCallback((id: string) => setSelected(id), [])
// React.memo — 純元件避免不必要 re-render
const ItemCard = React.memo<ItemCardProps>(({ item }) => (
<div>{item.name}</div>
))
// lazy + Suspense — 懶載入
const HeavyChart = lazy(() => import('./HeavyChart'))
<Suspense fallback={<Skeleton />}>
<HeavyChart data={data} />
</Suspense>
// Virtualization — 大型列表
import { useVirtualizer } from '@tanstack/react-virtual'
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null }
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <div>Something went wrong</div>
}
return this.props.children
}
}
// Usage
<ErrorBoundary fallback={<ErrorPage />}>
<App />
</ErrorBoundary>
const [form, setForm] = useState({ email: '', name: '' })
const [errors, setErrors] = useState<Record<string, string>>({})
function validate(): boolean {
const e: Record<string, string> = {}
if (!form.email.includes('@')) e.email = 'Invalid email'
if (!form.name.trim()) e.name = 'Name is required'
setErrors(e)
return Object.keys(e).length === 0
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!validate()) return
await submitForm(form)
}
如果專案有引入 Zod,可改用
schema.safeParse()取代手動 validate。
// ❌ 在 render 中建立物件/陣列(每次 re-render 都是新參考)
<Child style={{ color: 'red' }} items={[1, 2, 3]} />
// ✅ 提到外層或 useMemo
const style = useMemo(() => ({ color: 'red' }), [])
// ❌ useEffect 依賴陣列遺漏
useEffect(() => { fetchData(userId) }, []) // userId 變了不會重新 fetch
// ✅ 加入依賴
useEffect(() => { fetchData(userId) }, [userId])
// ❌ 直接修改 state
state.items.push(newItem)
// ✅ 不可變更新
setItems(prev => [...prev, newItem])
// ❌ 把所有東西塞進一個 useEffect
useEffect(() => { fetchA(); fetchB(); subscribe() }, [])
// ✅ 拆成獨立 effect
useEffect(() => { fetchA() }, [depA])
useEffect(() => { fetchB() }, [depB])
useEffect(() => { const unsub = subscribe(); return unsub }, [])
// ❌ 不必要的 state(可從現有 state 推導的值)
const [items, setItems] = useState([])
const [count, setCount] = useState(0) // 多餘
// ✅ 直接計算
const count = items.length