chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
@@ -0,0 +1,579 @@
# Generic Component Patterns
Generic components provide type safety while maintaining reusability. TypeScript infers generic type parameters from prop values — no manual type annotations needed at call site.
## Generic Table Component
Full-featured table with typed columns, custom rendering, sorting.
```typescript
type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], item: T) => React.ReactNode;
sortable?: boolean;
};
type TableProps<T> = {
data: T[];
columns: Column<T>[];
keyExtractor: (item: T) => string | number;
onSort?: (key: keyof T, direction: 'asc' | 'desc') => void;
className?: string;
};
function Table<T>({
data,
columns,
keyExtractor,
onSort,
className,
}: TableProps<T>) {
const [sortKey, setSortKey] = React.useState<keyof T | null>(null);
const [sortDir, setSortDir] = React.useState<'asc' | 'desc'>('asc');
const handleSort = (key: keyof T) => {
const newDir = sortKey === key && sortDir === 'asc' ? 'desc' : 'asc';
setSortKey(key);
setSortDir(newDir);
onSort?.(key, newDir);
};
return (
<table className={className}>
<thead>
<tr>
{columns.map((col) => (
<th key={String(col.key)}>
{col.sortable ? (
<button onClick={() => handleSort(col.key)}>
{col.header}
{sortKey === col.key && (sortDir === 'asc' ? ' ↑' : ' ↓')}
</button>
) : (
col.header
)}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((item) => (
<tr key={keyExtractor(item)}>
{columns.map((col) => (
<td key={String(col.key)}>
{col.render
? col.render(item[col.key], item)
: String(item[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
```
Usage:
```typescript
type User = {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
};
function UserTable({ users }: { users: User[] }) {
const [sortedUsers, setSortedUsers] = React.useState(users);
const handleSort = (key: keyof User, direction: 'asc' | 'desc') => {
const sorted = [...users].sort((a, b) => {
if (a[key] < b[key]) return direction === 'asc' ? -1 : 1;
if (a[key] > b[key]) return direction === 'asc' ? 1 : -1;
return 0;
});
setSortedUsers(sorted);
};
return (
<Table
data={sortedUsers}
columns={[
{ key: 'name', header: 'Name', sortable: true },
{
key: 'email',
header: 'Email',
render: (email) => <a href={`mailto:${email}`}>{email}</a>,
},
{
key: 'role',
header: 'Role',
render: (role) => (
<span className={role === 'admin' ? 'badge-admin' : 'badge-user'}>
{role}
</span>
),
},
{
key: 'createdAt',
header: 'Created',
render: (date) => new Date(date).toLocaleDateString(),
sortable: true,
},
]}
keyExtractor={(user) => user.id}
onSort={handleSort}
/>
);
}
```
## Generic Select/Dropdown
Type-safe select with custom option rendering, searching.
```typescript
type SelectProps<T> = {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string | number;
placeholder?: string;
disabled?: boolean;
searchable?: boolean;
};
function Select<T>({
options,
value,
onChange,
getLabel,
getValue,
placeholder = 'Select...',
disabled = false,
searchable = false,
}: SelectProps<T>) {
const [search, setSearch] = React.useState('');
const [isOpen, setIsOpen] = React.useState(false);
const filtered = searchable
? options.filter((opt) =>
getLabel(opt).toLowerCase().includes(search.toLowerCase())
)
: options;
const handleSelect = (option: T) => {
onChange(option);
setIsOpen(false);
setSearch('');
};
return (
<div className="select-wrapper">
<button
onClick={() => setIsOpen(!isOpen)}
disabled={disabled}
className="select-trigger"
>
{value ? getLabel(value) : placeholder}
</button>
{isOpen && (
<div className="select-dropdown">
{searchable && (
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
autoFocus
/>
)}
<ul>
{filtered.map((option) => (
<li
key={getValue(option)}
onClick={() => handleSelect(option)}
className={value === option ? 'selected' : ''}
>
{getLabel(option)}
</li>
))}
</ul>
</div>
)}
</div>
);
}
```
Usage:
```typescript
type Country = {
code: string;
name: string;
flag: string;
};
const countries: Country[] = [
{ code: 'US', name: 'United States', flag: '🇺🇸' },
{ code: 'CA', name: 'Canada', flag: '🇨🇦' },
{ code: 'MX', name: 'Mexico', flag: '🇲🇽' },
];
function CountrySelector() {
const [country, setCountry] = React.useState<Country | null>(null);
return (
<Select
options={countries}
value={country}
onChange={setCountry}
getLabel={(c) => `${c.flag} ${c.name}`}
getValue={(c) => c.code}
searchable
placeholder="Select country"
/>
);
}
```
## Generic List with Render Props
Flexible list rendering with type-safe render props.
```typescript
type ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
renderEmpty?: () => React.ReactNode;
keyExtractor: (item: T, index: number) => string | number;
className?: string;
as?: 'ul' | 'ol' | 'div';
};
function List<T>({
items,
renderItem,
renderEmpty,
keyExtractor,
className,
as: Component = 'ul',
}: ListProps<T>) {
if (items.length === 0 && renderEmpty) {
return <>{renderEmpty()}</>;
}
return (
<Component className={className}>
{items.map((item, index) => {
const key = keyExtractor(item, index);
const element = renderItem(item, index);
return Component === 'div' ? (
<div key={key}>{element}</div>
) : (
<li key={key}>{element}</li>
);
})}
</Component>
);
}
```
Usage:
```typescript
type Task = {
id: string;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
};
function TaskList({ tasks }: { tasks: Task[] }) {
return (
<List
items={tasks}
keyExtractor={(task) => task.id}
renderItem={(task) => (
<div className={`task priority-${task.priority}`}>
<input type="checkbox" checked={task.completed} readOnly />
<span className={task.completed ? 'completed' : ''}>{task.title}</span>
</div>
)}
renderEmpty={() => (
<div className="empty-state">No tasks yet. Add one to get started!</div>
)}
as="div"
/>
);
}
```
## Constrained Generic Components
Use constraints to ensure required properties or methods.
```typescript
// Constraint: items must have `id` property
type HasId = { id: string | number };
type GridProps<T extends HasId> = {
items: T[];
renderCard: (item: T) => React.ReactNode;
columns?: 2 | 3 | 4;
};
function Grid<T extends HasId>({
items,
renderCard,
columns = 3,
}: GridProps<T>) {
return (
<div className={`grid grid-cols-${columns}`}>
{items.map((item) => (
<div key={item.id} className="grid-item">
{renderCard(item)}
</div>
))}
</div>
);
}
// Usage - type must have `id` property
type Product = {
id: number;
name: string;
price: number;
image: string;
};
<Grid
items={products}
renderCard={(product) => (
<div>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
)}
columns={3}
/>;
```
## Multiple Generic Parameters
Components with multiple type parameters.
```typescript
type PairProps<K, V> = {
pairs: Array<[K, V]>;
renderKey: (key: K) => React.ReactNode;
renderValue: (value: V) => React.ReactNode;
};
function KeyValueList<K, V>({ pairs, renderKey, renderValue }: PairProps<K, V>) {
return (
<dl>
{pairs.map(([key, value], index) => (
<React.Fragment key={index}>
<dt>{renderKey(key)}</dt>
<dd>{renderValue(value)}</dd>
</React.Fragment>
))}
</dl>
);
}
// Usage
type MetricKey = 'cpu' | 'memory' | 'disk';
type MetricValue = { current: number; max: number };
const metrics: Array<[MetricKey, MetricValue]> = [
['cpu', { current: 45, max: 100 }],
['memory', { current: 8, max: 16 }],
['disk', { current: 250, max: 500 }],
];
<KeyValueList
pairs={metrics}
renderKey={(key) => <strong>{key.toUpperCase()}</strong>}
renderValue={(val) => (
<span>
{val.current}/{val.max}
</span>
)}
/>;
```
## Generic Form Field Component
Reusable form field with type-safe value handling.
```typescript
type FieldProps<T> = {
name: string;
label: string;
value: T;
onChange: (value: T) => void;
type: 'text' | 'number' | 'email' | 'password';
error?: string;
required?: boolean;
};
function FormField<T extends string | number>({
name,
label,
value,
onChange,
type,
error,
required = false,
}: FieldProps<T>) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue =
type === 'number' ? (Number(e.target.value) as T) : (e.target.value as T);
onChange(newValue);
};
return (
<div className="form-field">
<label htmlFor={name}>
{label}
{required && <span className="required">*</span>}
</label>
<input
id={name}
name={name}
type={type}
value={value}
onChange={handleChange}
required={required}
aria-invalid={!!error}
aria-describedby={error ? `${name}-error` : undefined}
/>
{error && (
<span id={`${name}-error`} className="error">
{error}
</span>
)}
</div>
);
}
// Usage
function UserForm() {
const [name, setName] = React.useState('');
const [age, setAge] = React.useState(0);
return (
<form>
<FormField
name="name"
label="Name"
value={name}
onChange={setName}
type="text"
required
/>
<FormField
name="age"
label="Age"
value={age}
onChange={setAge}
type="number"
/>
</form>
);
}
```
## Generic Modal/Dialog
Type-safe modal with generic result type.
```typescript
type ModalProps<T> = {
isOpen: boolean;
onClose: (result?: T) => void;
title: string;
children: (submit: (result: T) => void) => React.ReactNode;
};
function Modal<T>({ isOpen, onClose, title, children }: ModalProps<T>) {
if (!isOpen) return null;
const handleSubmit = (result: T) => {
onClose(result);
};
return (
<div className="modal-overlay">
<div className="modal">
<header>
<h2>{title}</h2>
<button onClick={() => onClose()}>×</button>
</header>
<div className="modal-body">{children(handleSubmit)}</div>
</div>
</div>
);
}
// Usage
type UserFormData = { name: string; email: string };
function App() {
const [showModal, setShowModal] = React.useState(false);
const [formData, setFormData] = React.useState<UserFormData>({
name: '',
email: '',
});
const handleModalClose = (result?: UserFormData) => {
if (result) {
console.log('User submitted:', result);
}
setShowModal(false);
};
return (
<>
<button onClick={() => setShowModal(true)}>Add User</button>
<Modal<UserFormData>
isOpen={showModal}
onClose={handleModalClose}
title="Add New User"
>
{(submit) => (
<form
onSubmit={(e) => {
e.preventDefault();
submit(formData);
}}
>
<input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Name"
/>
<input
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
)}
</Modal>
</>
);
}
```
@@ -0,0 +1,579 @@
# Server Components and Server Actions
React 19 Server Components run on server, can be async, enable zero-bundle data fetching. Server Actions handle mutations with progressive enhancement.
## Async Server Component
Server Components can be async functions — await data fetching directly.
```typescript
// app/users/[id]/page.tsx
type PageProps = {
params: { id: string };
searchParams?: { tab?: string; edit?: string };
};
export default async function UserPage({ params, searchParams }: PageProps) {
// Runs on server - no client bundle
const user = await fetchUser(params.id);
const posts = await fetchUserPosts(params.id);
return (
<div>
<header>
<h1>{user.name}</h1>
<p>{user.email}</p>
</header>
<UserTabs user={user} posts={posts} activeTab={searchParams?.tab} />
{searchParams?.edit === 'true' && (
<UserEditForm user={user} />
)}
</div>
);
}
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`, {
cache: 'no-store', // Or 'force-cache', 'revalidate'
});
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
}
```
## Parallel Data Fetching
Fetch multiple resources in parallel with Promise.all.
```typescript
type DashboardProps = {
params: { userId: string };
};
export default async function Dashboard({ params }: DashboardProps) {
// Parallel fetching
const [user, stats, activity] = await Promise.all([
fetchUser(params.userId),
fetchUserStats(params.userId),
fetchRecentActivity(params.userId),
]);
return (
<div>
<UserHeader user={user} />
<StatsGrid stats={stats} />
<ActivityFeed items={activity} />
</div>
);
}
```
## Sequential vs Waterfall Fetching
```typescript
// ❌ Waterfall - slow
async function SlowPage() {
const user = await fetchUser('123');
const posts = await fetchUserPosts(user.id); // Waits for user
const comments = await fetchPostComments(posts[0].id); // Waits for posts
return <div>...</div>;
}
// ✅ Parallel - fast
async function FastPage() {
const userPromise = fetchUser('123');
const postsPromise = fetchUserPosts('123');
const [user, posts] = await Promise.all([userPromise, postsPromise]);
// If comments depend on posts, fetch after
const comments = await fetchPostComments(posts[0].id);
return <div>...</div>;
}
```
## Server Actions - Form Mutations
Server Actions marked with 'use server' — run on server, callable from client.
```typescript
// actions/user.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const updateUserSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
bio: z.string().max(500, 'Bio must be less than 500 characters').optional(),
});
type FormState = {
success?: boolean;
errors?: Record<string, string[]>;
message?: string;
};
export async function updateUser(
userId: string,
prevState: FormState,
formData: FormData
): Promise<FormState> {
// Validate
const parsed = updateUserSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
bio: formData.get('bio'),
});
if (!parsed.success) {
return {
success: false,
errors: parsed.error.flatten().fieldErrors,
};
}
try {
// Mutate database
await db.user.update({
where: { id: userId },
data: parsed.data,
});
// Revalidate cached data
revalidatePath(`/users/${userId}`);
revalidateTag(`user-${userId}`);
return { success: true, message: 'Profile updated successfully' };
} catch (error) {
return {
success: false,
message: 'Failed to update profile. Please try again.',
};
}
}
export async function deleteUser(userId: string) {
await db.user.delete({ where: { id: userId } });
revalidatePath('/users');
redirect('/users'); // Navigate after mutation
}
```
## Client Component Using Server Action
```typescript
// components/UserForm.tsx
'use client';
import { useActionState } from 'react';
import { updateUser } from '@/actions/user';
type FormState = {
success?: boolean;
errors?: Record<string, string[]>;
message?: string;
};
export function UserEditForm({ userId, initialData }: Props) {
const [state, formAction, isPending] = useActionState<FormState, FormData>(
(prevState, formData) => updateUser(userId, prevState, formData),
{}
);
return (
<form action={formAction}>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
defaultValue={initialData.name}
required
aria-invalid={!!state.errors?.name}
/>
{state.errors?.name?.map((error) => (
<p key={error} className="error">
{error}
</p>
))}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
defaultValue={initialData.email}
required
aria-invalid={!!state.errors?.email}
/>
{state.errors?.email?.map((error) => (
<p key={error} className="error">
{error}
</p>
))}
</div>
<div>
<label htmlFor="bio">Bio</label>
<textarea
id="bio"
name="bio"
defaultValue={initialData.bio}
aria-invalid={!!state.errors?.bio}
/>
{state.errors?.bio?.map((error) => (
<p key={error} className="error">
{error}
</p>
))}
</div>
{state.message && (
<div className={state.success ? 'success' : 'error'}>{state.message}</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save Changes'}
</button>
</form>
);
}
```
## Programmatic Server Action
Call Server Actions directly from client code, not just forms.
```typescript
'use client';
import { deleteUser } from '@/actions/user';
import { useTransition } from 'react';
export function DeleteButton({ userId }: { userId: string }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
if (!confirm('Are you sure you want to delete this user?')) return;
startTransition(async () => {
try {
await deleteUser(userId);
// deleteUser calls redirect(), navigation happens automatically
} catch (error) {
console.error('Failed to delete user:', error);
}
});
};
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete User'}
</button>
);
}
```
## use() Hook - Unwrapping Promises
Pass promises from Server to Client components, unwrap with use().
```typescript
// Server Component
async function UserPage({ params }: { params: { id: string } }) {
// Don't await - pass promise to client
const userPromise = fetchUser(params.id);
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component
'use client';
import { use } from 'react';
type Props = {
userPromise: Promise<User>;
};
export function UserProfile({ userPromise }: Props) {
// Suspends until resolved
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
```
## use() with Context
use() also unwraps context — alternative to useContext.
```typescript
'use client';
import { use } from 'react';
import { ThemeContext } from './ThemeProvider';
export function ThemedButton() {
const theme = use(ThemeContext); // Same as useContext(ThemeContext)
return <button className={theme.mode}>{theme.primaryColor}</button>;
}
```
## Streaming with Suspense
Stream components as they resolve — faster initial page load.
```typescript
// Server Component
export default async function Page() {
return (
<div>
<h1>Dashboard</h1>
{/* Renders immediately */}
<StaticContent />
{/* Streams when ready */}
<Suspense fallback={<Spinner />}>
<SlowComponent />
</Suspense>
{/* Independent stream */}
<Suspense fallback={<Skeleton />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
async function SlowComponent() {
const data = await slowFetch(); // Takes 2s
return <div>{data}</div>;
}
async function AnotherSlowComponent() {
const data = await anotherSlowFetch(); // Takes 1s
return <div>{data}</div>;
}
```
## Error Handling in Server Components
Use error.tsx for error boundaries.
```typescript
// app/users/[id]/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// Server Component throws error
export default async function UserPage({ params }: Props) {
const user = await fetchUser(params.id);
if (!user) {
throw new Error('User not found'); // Caught by error.tsx
}
return <div>{user.name}</div>;
}
```
## Loading States with loading.tsx
```typescript
// app/users/[id]/loading.tsx
export default function Loading() {
return <UserSkeleton />;
}
// Automatically wraps page in Suspense
// No need for manual Suspense boundary
```
## Server-Only Code
Ensure code never runs on client.
```typescript
// lib/server-only-utils.ts
import 'server-only'; // Throws if imported in client component
export async function getSecretKey() {
return process.env.SECRET_KEY; // Safe - never in client bundle
}
export async function hashPassword(password: string) {
const bcrypt = await import('bcrypt');
return bcrypt.hash(password, 10);
}
```
## Client-Only Code
Ensure code never runs on server.
```typescript
// lib/client-only-utils.ts
import 'client-only';
export function useLocalStorage(key: string) {
// localStorage only available in browser
const [value, setValue] = useState(() => localStorage.getItem(key));
useEffect(() => {
localStorage.setItem(key, value || '');
}, [key, value]);
return [value, setValue] as const;
}
```
## Mixing Server and Client Components
```typescript
// app/page.tsx (Server Component)
export default async function Page() {
const data = await fetchData();
return (
<div>
{/* Server Component - can be async */}
<ServerComponent data={data} />
{/* Client Component - interactive */}
<ClientComponent initialData={data} />
</div>
);
}
// ServerComponent.tsx (Server Component - default)
export function ServerComponent({ data }: { data: Data }) {
return <div>{data.title}</div>;
}
// ClientComponent.tsx (Client Component)
'use client';
export function ClientComponent({ initialData }: { initialData: Data }) {
const [data, setData] = useState(initialData);
return (
<button onClick={() => setData({ ...data, count: data.count + 1 })}>
{data.count}
</button>
);
}
```
## Server Component Patterns
```typescript
// ✅ Server Component can:
// - Be async
// - Fetch data directly
// - Access backend resources (DB, filesystem)
// - Use server-only packages
// - Pass serializable props to client components
export default async function Page() {
const db = await connectDB(); // Direct DB access
const users = await db.user.findMany();
return <UserList users={users} />; // Pass serializable data
}
// ❌ Server Component cannot:
// - Use hooks (useState, useEffect, etc)
// - Use browser APIs (localStorage, window, etc)
// - Add event listeners (onClick, onChange, etc)
// - Use React Context
// ✅ Client Component can:
// - Use hooks
// - Use browser APIs
// - Add event listeners
// - Use React Context
// - Import Server Components as children
'use client';
export function Layout({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light');
return (
<div className={theme}>
<button onClick={() => setTheme('dark')}>Toggle</button>
{children} {/* Server Component can be child */}
</div>
);
}
// ❌ Client Component cannot:
// - Be async
// - Directly access backend resources
// - Import server-only packages
```
## Progressive Enhancement with Server Actions
Forms work without JavaScript when using Server Actions.
```typescript
// components/AddTodoForm.tsx
import { addTodo } from '@/actions/todos';
export function AddTodoForm() {
return (
<form action={addTodo}>
<input name="title" required />
<button type="submit">Add Todo</button>
{/* Works without JS - progressive enhancement */}
</form>
);
}
// actions/todos.ts
'use server';
export async function addTodo(formData: FormData) {
const title = formData.get('title');
if (typeof title !== 'string') return;
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
```