React frontend standards covering hooks (useState, useEffect, useContext, custom hooks), state management (Context API, Redux, Zustand), performance optimization (memoization, lazy loading, code splitting), testing with React Testing Library, and accessibility (WCAG 2.1, ARIA) for modern SPAs
// State Hook
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Effect Hook
useEffect(() => {
// Side effect
return () => {/* cleanup */};
}, [dependencies]);
// Context Hook
const theme = useContext(ThemeContext);
// Ref Hook
const inputRef = useRef<HTMLInputElement>(null);
// Memo Hooks
const value = useMemo(() => expensiveCalc(a, b), [a, b]);
const callback = useCallback(() => doSomething(a), [a]);
Is state needed in >2 components?
NO -> Use local state (useState)
YES -> Is it global UI state (theme, auth)?
YES -> Use Context API
NO -> Is it server data (API responses)?
YES -> Use React Query/TanStack Query
NO -> Complex state with many actions?
YES -> Use Redux Toolkit or Zustand
NO -> Use Context API + useReducer
# Create React app with TypeScript
npx create-react-app my-app --template typescript
# Install testing library
npm install --save-dev @testing-library/react @testing-library/jest-dom
# Install React Query for server state
npm install @tanstack/react-query
# State management (choose one)
npm install zustand # Lightweight
npm install @reduxjs/toolkit # Enterprise
// Lazy initialization for expensive initial state
const [data, setData] = useState(() => computeExpensiveValue());
// Functional updates when new state depends on old
setCount(prevCount => prevCount + 1);
// Component mount (runs once)
useEffect(() => {
console.log('Component mounted');
}, []);
// Cleanup pattern for async operations
useEffect(() => {
let isMounted = true;
async function fetchData() {
const result = await api.getData();
if (isMounted) setData(result);
}
fetchData();
return () => { isMounted = false; };
}, []);
// Subscription cleanup
useEffect(() => {
const subscription = api.subscribe(userId);
return () => subscription.unsubscribe();
}, [userId]);
// Create context with TypeScript
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Custom hook for consuming context (always use this pattern)
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error('useTheme must be used within ThemeProvider');
return context;
};
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
default: return state;
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });
// Always prefix with 'use'
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let isMounted = true;
fetch(url)
.then(res => res.json())
.then(data => { if (isMounted) { setData(data); setLoading(false); }})
.catch(err => { if (isMounted) { setError(err); setLoading(false); }});
return () => { isMounted = false; };
}, [url]);
return { data, loading, error };
}
// Split contexts to reduce re-renders
<AuthProvider>
<ThemeProvider>
<LocaleProvider>
<App />
</LocaleProvider>
</ThemeProvider>
</AuthProvider>
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function Users() {
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
});
}
import create from 'zustand';
const useBearStore = create<BearState>((set) => ({
bears: 0,
increase: () => set((state) => ({ bears: state.bears + 1 })),
}));
// Usage (no Provider needed!)
const bears = useBearStore((state) => state.bears);
const ExpensiveComponent = React.memo(({ data }: Props) => {
return <div>{/* Expensive render */}</div>;
});
const Home = lazy(() => import('./pages/Home'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Suspense>
);
}
import { FixedSizeList } from 'react-window';
<FixedSizeList height={600} itemCount={10000} itemSize={35} width="100%">
{({ index, style }) => <div style={style}>Row {index}</div>}
</FixedSizeList>
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
export function Button({ variant = 'primary', size = 'md', children }: ButtonProps) {
return <button className={`btn-${variant} btn-${size}`}>{children}</button>;
}
const TabsContext = createContext<TabsContextType | undefined>(undefined);
export function Tabs({ children }: { children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState('tab1');
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
Tabs.Tab = function Tab({ id, children }: TabProps) {
const context = useContext(TabsContext);
return (
<button onClick={() => context?.setActiveTab(id)}>
{children}
</button>
);
};
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('user can submit form', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<ContactForm onSubmit={onSubmit} />);
const nameInput = screen.getByRole('textbox', { name: /name/i });
await user.type(nameInput, 'John Doe');
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe' });
});
});
import { renderHook, act } from '@testing-library/react';
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1);
});
<nav><ul><li><a href="/">Home</a></li></ul></nav>
<main><article><h1>Title</h1><section><h2>Section</h2></section></article></main>
<footer>© 2024 Company</footer>
<button aria-label="Close dialog" onClick={onClose}>X</button>
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Confirm Action</h2>
</div>
<div role="alert" aria-live="assertive">{error}</div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && <span id="email-error" role="alert">{errors.email}</span>}
// React escapes by default - safe
<div>{userInput}</div>
// If dangerouslySetInnerHTML needed, use DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHTML);
// Prefer httpOnly cookies (backend sets it)
// If localStorage required: short expiry, refresh tokens, clear on logout
See REFERENCE.md for:
npx skills add williamzujkowski/react-frontend下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer