AgentSkillsCN

React Patterns

React 设计模式

SKILL.md

React Patterns Skill

Modern React 19 patterns for scalable applications

PRINCIPLES

  1. Composition over inheritance: Build from small, focused components
  2. Single responsibility: One component, one job
  3. Lift state up: Share state via closest common ancestor
  4. Colocation: Keep state close to where it's used

HOOKS PATTERNS

useState Best Practices

typescript
// ✅ Functional updates for derived state
const [count, setCount] = useState(0);
setCount(prev => prev + 1); // Always use prev when depending on current

// ✅ Lazy initialization for expensive computations
const [data, setData] = useState(() => {
  return expensiveComputation(); // Only runs once
});

// ✅ Object state with proper updates
const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, name: 'John' })); // Merge, don't replace

useEffect Patterns

typescript
// ✅ Cleanup pattern
useEffect(() => {
  const subscription = api.subscribe(handler);
  
  return () => {
    subscription.unsubscribe(); // Always cleanup
  };
}, [handler]);

// ✅ Abort controller for fetch
useEffect(() => {
  const controller = new AbortController();
  
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });
  
  return () => controller.abort();
}, []);

// ✅ Sync external system
useEffect(() => {
  const map = mapRef.current;
  map.setCenter(center);
}, [center]);

useCallback & useMemo

typescript
// ✅ useCallback for stable function reference
const handleSubmit = useCallback((data: FormData) => {
  onSubmit(data);
}, [onSubmit]);

// ✅ useMemo for expensive calculations
const sortedItems = useMemo(() => {
  return items.slice().sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// ✅ useMemo for stable object reference
const style = useMemo(() => ({
  color: theme.primary,
  fontSize: size,
}), [theme.primary, size]);

useTransition (React 19)

typescript
'use client';

import { useTransition } from 'react';

function SearchResults({ query }: { query: string }) {
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState<Result[]>([]);
  
  const handleSearch = (newQuery: string) => {
    // Mark as non-urgent update
    startTransition(async () => {
      const data = await searchAPI(newQuery);
      setResults(data);
    });
  };
  
  return (
    <div>
      <input onChange={e => handleSearch(e.target.value)} />
      {isPending ? <Spinner /> : <ResultList results={results} />}
    </div>
  );
}

Custom Hooks

typescript
// ✅ Extract reusable logic
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;
    
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  const setValue = useCallback((value: T | ((val: T) => T)) => {
    setStoredValue(prev => {
      const valueToStore = value instanceof Function ? value(prev) : value;
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
      return valueToStore;
    });
  }, [key]);
  
  return [storedValue, setValue] as const;
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');

COMPONENT PATTERNS

Compound Components

typescript
// Compound component pattern for related UI
interface TabsContextValue {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabsContext = createContext<TabsContextValue | null>(null);

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }: { children: ReactNode }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ value, children }: { value: string; children: ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('Tab must be inside Tabs');
  
  return (
    <button
      role="tab"
      aria-selected={context.activeTab === value}
      onClick={() => context.setActiveTab(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ value, children }: { value: string; children: ReactNode }) {
  const context = useContext(TabsContext);
  if (!context) throw new Error('TabPanel must be inside Tabs');
  
  if (context.activeTab !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

// Attach sub-components
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
<Tabs defaultTab="profile">
  <Tabs.List>
    <Tabs.Tab value="profile">Profile</Tabs.Tab>
    <Tabs.Tab value="settings">Settings</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="profile">Profile content</Tabs.Panel>
  <Tabs.Panel value="settings">Settings content</Tabs.Panel>
</Tabs>

Render Props

typescript
// Render prop pattern for flexible rendering
interface FetchProps<T> {
  url: string;
  children: (data: T | null, loading: boolean, error: Error | null) => ReactNode;
}

function Fetch<T>({ url, children }: FetchProps<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);
  
  return <>{children(data, loading, error)}</>;
}

// Usage
<Fetch<User[]> url="/api/users">
  {(users, loading, error) => {
    if (loading) return <Spinner />;
    if (error) return <Error message={error.message} />;
    return <UserList users={users!} />;
  }}
</Fetch>

Slot Pattern

typescript
// Slot pattern for flexible layouts
interface CardProps {
  children: ReactNode;
  header?: ReactNode;
  footer?: ReactNode;
}

function Card({ children, header, footer }: CardProps) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  );
}

// Usage
<Card
  header={<h2>Title</h2>}
  footer={<Button>Save</Button>}
>
  Card content here
</Card>

PERFORMANCE PATTERNS

React.memo

typescript
// Memoize when props rarely change
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

// Custom comparison function
const UserCard = memo(
  function UserCard({ user, onSelect }: Props) {
    return <div onClick={() => onSelect(user.id)}>{user.name}</div>;
  },
  (prev, next) => prev.user.id === next.user.id
);

Code Splitting

typescript
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={data} />
      </Suspense>
    </div>
  );
}

// Route-based splitting (Next.js handles this)
const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Skeleton />,
  ssr: false, // Client-only
});

Virtual Lists

typescript
// For long lists, use virtualization
import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
  });
  
  return (
    <div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: virtualRow.start,
              height: virtualRow.size,
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

STATE MANAGEMENT

Context Pattern

typescript
// Create typed context with reducer
interface State {
  user: User | null;
  theme: 'light' | 'dark';
}

type Action = 
  | { type: 'SET_USER'; payload: User }
  | { type: 'LOGOUT' }
  | { type: 'TOGGLE_THEME' };

const AppContext = createContext<{
  state: State;
  dispatch: Dispatch<Action>;
} | null>(null);

function appReducer(state: State, action: Action): State {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'LOGOUT':
      return { ...state, user: null };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    default:
      return state;
  }
}

export function AppProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(appReducer, { user: null, theme: 'light' });
  
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
}

export function useApp() {
  const context = useContext(AppContext);
  if (!context) throw new Error('useApp must be inside AppProvider');
  return context;
}

ANTI-PATTERNS

❌ Avoid

typescript
// ❌ Derived state in useState
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]); // Redundant!

// ❌ Object identity in deps
useEffect(() => {
  // Runs every render!
}, [{ id: 1 }]);

// ❌ Missing cleanup
useEffect(() => {
  window.addEventListener('resize', handler);
  // No cleanup!
}, []);

// ❌ State for everything
const [x, setX] = useState(props.x); // Just use props.x!

✅ Prefer

typescript
// ✅ Derive state from existing
const filteredItems = useMemo(
  () => items.filter(i => i.active),
  [items]
);

// ✅ Stable reference in deps
const config = useMemo(() => ({ id: 1 }), []);
useEffect(() => {}, [config]);

// ✅ Always cleanup
useEffect(() => {
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, [handler]);

// ✅ Use props directly when possible
function Component({ x }: Props) {
  return <div>{x}</div>; // No state needed
}

QUICK REFERENCE

PatternWhen to Use
useStateLocal component state
useReducerComplex state logic
useContextGlobal/shared state
useMemoExpensive calculations
useCallbackStable function references
useTransitionNon-urgent updates
memoPrevent unnecessary rerenders
Compound ComponentsRelated UI components
Render PropsFlexible rendering logic