Frontend Builder by daffy0208/ai-dev-standards
npx skills add https://github.com/daffy0208/ai-dev-standards --skill 'Frontend Builder'构建可维护、高性能的 React 和 Next.js 前端应用。
将 UI 拆分为小型、可复用、单一职责的组件
尽可能将状态保持在离使用位置最近的地方
优化渲染、代码分割和资源加载
清晰的命名、一致的模式、有用的错误提示
在以下情况使用 React + Vite:
在以下情况使用 Next.js:
大多数项目推荐:Next.js (App Router)
1. 页面组件 (路由入口点):
// app/users/page.tsx (Next.js App Router)
export default function UsersPage() {
return (
<div>
<Header />
<UserList />
<Footer />
</div>
)
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
2. 功能组件 (业务逻辑):
// components/features/UserList.tsx
export function UserList() {
const { data, isLoading } = useUsers()
if (isLoading) return <LoadingSpinner />
return (
<div>
{data.map(user => <UserCard key={user.id} user={user} />)}
</div>
)
}
3. UI 组件 (可复用,无业务逻辑):
// components/ui/button.tsx
export function Button({ children, variant = 'primary', ...props }) {
return (
<button
className={cn(buttonVariants[variant])}
{...props}
>
{children}
</button>
)
}
// ✅ 良好:小巧、专注、类型化
interface UserProfileProps {
user: User
onEdit?: () => void
}
export function UserProfile({ user, onEdit }: UserProfileProps) {
return (
<div className="flex gap-4">
<Avatar src={user.avatar} alt={user.name} />
<UserDetails user={user} />
{onEdit && <Button onClick={onEdit}>Edit</Button>}
</div>
)
}
// ❌ 糟糕:庞大、无类型、不清晰
export function UserProfile(props) {
// 500 行 JSX,多重职责
return <div>...</div>
}
How many components need this state?
│
├─ One component → useState
├─ Parent + children → Props or useState + props
├─ Siblings → Lift to common parent
├─ Widely used (theme, auth) → Context API
└─ Complex app state → Zustand or Redux
// 用于组件级状态
function Counter() {
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}
// 用于应用级状态(主题、认证、用户)
const UserContext = createContext<UserContextType | undefined>(undefined)
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
const context = useContext(UserContext)
if (!context) throw new Error('useUser must be within UserProvider')
return context
}
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
// 使用
function Counter() {
const { count, increment } = useCounterStore()
return <button onClick={increment}>{count}</button>
}
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// 查询 (GET)
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000 // 5 分钟
})
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
return <UserList users={data} />
}
// 变更 (POST, PUT, DELETE)
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
return (
<button onClick={() => mutation.mutate({ name: 'John' })}>
Create User
</button>
)
}
// app/users/page.tsx
// 服务端组件 - 在服务器上获取数据
export default async function UsersPage() {
const users = await fetchUsers() // 在服务器上运行
return <UserList users={users} />
}
// 客户端组件 - 用于交互
'use client'
export function UserList({ users }: { users: User[] }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => setSelected(user.id)}
/>
))}
</div>
)
}
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters')
})
type LoginForm = z.infer<typeof loginSchema>
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema)
})
const onSubmit = async (data: LoginForm) => {
await login(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
className="border p-2"
/>
{errors.email && (
<span className="text-red-500">{errors.email.message}</span>
)}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Password"
className="border p-2"
/>
{errors.password && (
<span className="text-red-500">{errors.password.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
// 安装: @shadcn/ui 用于组件库
function Button({ variant = 'primary', children, ...props }) {
return (
<button
className={cn(
'px-4 py-2 rounded font-medium transition-colors',
{
'bg-blue-500 text-white hover:bg-blue-600': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-500 text-white hover:bg-red-600': variant === 'danger'
}
)}
{...props}
>
{children}
</button>
)
}
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary {
background-color: blue;
color: white;
}
// Button.tsx
import styles from './Button.module.css'
export function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
)
}
import { memo, useMemo, useCallback } from 'react'
// 1. 记忆化昂贵的计算
function DataTable({ data }) {
const sortedData = useMemo(
() => data.sort((a, b) => a.name.localeCompare(b.name)),
[data]
)
return <Table data={sortedData} />
}
// 2. 记忆化回调函数
function Parent() {
const handleClick = useCallback(() => {
console.log('Clicked')
}, [])
return <ExpensiveChild onClick={handleClick} />
}
// 3. 记忆化组件
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
return <button onClick={onClick}>Click</button>
})
// 1. 图片优化
import Image from 'next/image'
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
priority // 首屏优先加载
/>
// 2. 字体优化
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
// 3. 动态导入 (代码分割)
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <LoadingSpinner />
})
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200">
<h2 className="text-red-800">Something went wrong</h2>
<p className="text-red-600">{this.state.error?.message}</p>
</div>
)
}
return this.props.children
}
}
// app/error.tsx
'use client'
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
app/
├── (auth)/ # 路由组 (认证页面)
│ ├── login/
│ └── signup/
├── (dashboard)/ # 路由组 (仪表板)
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings/
├── api/ # API 路由
│ └── users/
│ └── route.ts
└── layout.tsx # 根布局
components/
├── ui/ # shadcn/ui 组件
│ ├── button.tsx
│ ├── input.tsx
│ └── dialog.tsx
├── features/ # 功能组件
│ ├── UserList.tsx
│ └── UserProfile.tsx
└── layouts/ # 布局组件
├── Header.tsx
└── Footer.tsx
lib/
├── utils.ts # 工具函数
├── api.ts # API 客户端
└── validation.ts # Zod 模式
hooks/
├── useUser.ts
└── useDebounce.ts
stores/
└── userStore.ts # Zustand 存储
// 1. 类型化组件属性
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
children: ReactNode
onClick?: () => void
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>
}
// 2. 类型化 API 响应
interface User {
id: string
name: string
email: string
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch('/api/users')
return res.json()
}
// 3. 类型化状态
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
优秀的前端应用:
相关技能:
api-designer - 用于设计要消费的后端 APIux-designer - 用于创建要实现的 UX 设计deployment-advisor - 用于托管 Next.js/React 应用相关模式:
META/DECISION-FRAMEWORK.md - 前端框架选择STANDARDS/architecture-patterns/component-patterns.md - 组件设计模式 (创建时)相关操作手册:
PLAYBOOKS/setup-nextjs-project.md - Next.js 项目设置 (创建时)PLAYBOOKS/optimize-frontend-performance.md - 性能优化 (创建时)每周安装
–
代码仓库
GitHub 星标
21
首次出现
–
安全审计
Build maintainable, performant React and Next.js frontends.
Break UI into small, reusable, single-purpose components
Keep state as close to where it's used as possible
Optimize rendering, code splitting, and asset loading
Clear naming, consistent patterns, helpful errors
Use React + Vite when :
Use Next.js when :
Recommended for most projects : Next.js (App Router)
1. Page Components (Route entry points):
// app/users/page.tsx (Next.js App Router)
export default function UsersPage() {
return (
<div>
<Header />
<UserList />
<Footer />
</div>
)
}
2. Feature Components (Business logic):
// components/features/UserList.tsx
export function UserList() {
const { data, isLoading } = useUsers()
if (isLoading) return <LoadingSpinner />
return (
<div>
{data.map(user => <UserCard key={user.id} user={user} />)}
</div>
)
}
3. UI Components (Reusable, no business logic):
// components/ui/button.tsx
export function Button({ children, variant = 'primary', ...props }) {
return (
<button
className={cn(buttonVariants[variant])}
{...props}
>
{children}
</button>
)
}
// ✅ Good: Small, focused, typed
interface UserProfileProps {
user: User
onEdit?: () => void
}
export function UserProfile({ user, onEdit }: UserProfileProps) {
return (
<div className="flex gap-4">
<Avatar src={user.avatar} alt={user.name} />
<UserDetails user={user} />
{onEdit && <Button onClick={onEdit}>Edit</Button>}
</div>
)
}
// ❌ Bad: Giant, untyped, unclear
export function UserProfile(props) {
// 500 lines of JSX, multiple responsibilities
return <div>...</div>
}
How many components need this state?
│
├─ One component → useState
├─ Parent + children → Props or useState + props
├─ Siblings → Lift to common parent
├─ Widely used (theme, auth) → Context API
└─ Complex app state → Zustand or Redux
// For component-level state
function Counter() {
const [count, setCount] = useState(0)
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}
// For app-wide state (theme, auth, user)
const UserContext = createContext<UserContextType | undefined>(undefined)
export function UserProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
const context = useContext(UserContext)
if (!context) throw new Error('useUser must be within UserProvider')
return context
}
import { create } from 'zustand'
interface CounterStore {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
// Usage
function Counter() {
const { count, increment } = useCounterStore()
return <button onClick={increment}>{count}</button>
}
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Query (GET)
function Users() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000 // 5 minutes
})
if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
return <UserList users={data} />
}
// Mutation (POST, PUT, DELETE)
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
return (
<button onClick={() => mutation.mutate({ name: 'John' })}>
Create User
</button>
)
}
// app/users/page.tsx
// Server Component - fetches on server
export default async function UsersPage() {
const users = await fetchUsers() // Runs on server
return <UserList users={users} />
}
// Client Component - for interactivity
'use client'
export function UserList({ users }: { users: User[] }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={() => setSelected(user.id)}
/>
))}
</div>
)
}
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters')
})
type LoginForm = z.infer<typeof loginSchema>
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema)
})
const onSubmit = async (data: LoginForm) => {
await login(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
className="border p-2"
/>
{errors.email && (
<span className="text-red-500">{errors.email.message}</span>
)}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Password"
className="border p-2"
/>
{errors.password && (
<span className="text-red-500">{errors.password.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
// Install: @shadcn/ui for component library
function Button({ variant = 'primary', children, ...props }) {
return (
<button
className={cn(
'px-4 py-2 rounded font-medium transition-colors',
{
'bg-blue-500 text-white hover:bg-blue-600': variant === 'primary',
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
'bg-red-500 text-white hover:bg-red-600': variant === 'danger'
}
)}
{...props}
>
{children}
</button>
)
}
// Button.module.css
.button {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
}
.primary {
background-color: blue;
color: white;
}
// Button.tsx
import styles from './Button.module.css'
export function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
)
}
import { memo, useMemo, useCallback } from 'react'
// 1. Memoize expensive calculations
function DataTable({ data }) {
const sortedData = useMemo(
() => data.sort((a, b) => a.name.localeCompare(b.name)),
[data]
)
return <Table data={sortedData} />
}
// 2. Memoize callbacks
function Parent() {
const handleClick = useCallback(() => {
console.log('Clicked')
}, [])
return <ExpensiveChild onClick={handleClick} />
}
// 3. Memoize components
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
return <button onClick={onClick}>Click</button>
})
// 1. Image optimization
import Image from 'next/image'
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
priority // Above the fold
/>
// 2. Font optimization
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
)
}
// 3. Dynamic imports (code splitting)
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <LoadingSpinner />
})
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-4 bg-red-50 border border-red-200">
<h2 className="text-red-800">Something went wrong</h2>
<p className="text-red-600">{this.state.error?.message}</p>
</div>
)
}
return this.props.children
}
}
// app/error.tsx
'use client'
export default function Error({
error,
reset
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
app/
├── (auth)/ # Route group (auth pages)
│ ├── login/
│ └── signup/
├── (dashboard)/ # Route group (dashboard)
│ ├── layout.tsx
│ ├── page.tsx
│ └── settings/
├── api/ # API routes
│ └── users/
│ └── route.ts
└── layout.tsx # Root layout
components/
├── ui/ # shadcn/ui components
│ ├── button.tsx
│ ├── input.tsx
│ └── dialog.tsx
├── features/ # Feature components
│ ├── UserList.tsx
│ └── UserProfile.tsx
└── layouts/ # Layout components
├── Header.tsx
└── Footer.tsx
lib/
├── utils.ts # Utility functions
├── api.ts # API client
└── validation.ts # Zod schemas
hooks/
├── useUser.ts
└── useDebounce.ts
stores/
└── userStore.ts # Zustand stores
// 1. Type component props
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
children: ReactNode
onClick?: () => void
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>
}
// 2. Type API responses
interface User {
id: string
name: string
email: string
}
async function fetchUsers(): Promise<User[]> {
const res = await fetch('/api/users')
return res.json()
}
// 3. Type state
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
Great frontends:
Related Skills :
api-designer - For designing backend APIs to consumeux-designer - For creating UX designs to implementdeployment-advisor - For hosting Next.js/React appsRelated Patterns :
META/DECISION-FRAMEWORK.md - Frontend framework selectionSTANDARDS/architecture-patterns/component-patterns.md - Component design patterns (when created)Related Playbooks :
PLAYBOOKS/setup-nextjs-project.md - Next.js project setup (when created)PLAYBOOKS/optimize-frontend-performance.md - Performance optimization (when created)Weekly Installs
–
Repository
GitHub Stars
21
First Seen
–
Security Audits
UI组件模式实战指南:构建可复用React组件库与设计系统
10,700 周安装