一键导入
developing-with-react
React 18+ development with hooks, state management, component patterns, and accessibility
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
React 18+ development with hooks, state management, component patterns, and accessibility
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Third-party API integration patterns with webhook verification, idempotency, retry with Polly, circuit breakers, and HttpClientFactory. Use when integrating external services like Stripe, Twilio, SendGrid, or HubSpot.
Third-party API integration patterns with webhook verification, idempotency, retry with Polly, circuit breakers, and HttpClientFactory. Use when integrating external services like Stripe, Twilio, SendGrid, or HubSpot.
.NET 9 development with Clean Architecture, MediatR CQRS, Entity Framework Core, minimal APIs, and dependency injection. Use when writing C# code or working with .NET projects.
Azure DevOps YAML pipelines with multi-stage deployments, template references, variable groups, and environment approvals. Use when building CI/CD pipelines in Azure DevOps.
Azure DevOps YAML pipelines with multi-stage deployments, template references, variable groups, and environment approvals. Use when building CI/CD pipelines in Azure DevOps.
Production browser automation with Playwright for RPA, web scraping, and workflow automation. Resilient selectors, session persistence, retry patterns, and Playwright 1.56 agents. Distinct from E2E testing.
| name | developing-with-react |
| description | React 18+ development with hooks, state management, component patterns, and accessibility |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
Production-grade React development following strict quality standards. Prefer accessibility and maintainability over convenience and cleverness.
React 18+ functional components with hooks, state management, and styling. Testing is covered in the jest skill.
Organize components with clear separation of concerns.
// 1. Imports (grouped: react, external, internal, types)
import { useState, useCallback, type FC } from 'react';
import { useUserData } from '@/hooks/useUserData';
import { Button } from '@/components/Button';
import type { User } from '@/types';
import styles from './UserCard.module.css';
// 2. Types
interface UserCardProps {
userId: string;
onSelect?: (user: User) => void;
}
// 3. Component
export const UserCard: FC<UserCardProps> = ({ userId, onSelect }) => {
// 4. Hooks (state, effects, custom hooks)
const { user, loading, error } = useUserData(userId);
const [isExpanded, setIsExpanded] = useState(false);
// 5. Event handlers
const handleToggle = useCallback(() => {
setIsExpanded((prev) => !prev);
}, []);
const handleSelect = useCallback(() => {
if (user && onSelect) {
onSelect(user);
}
}, [user, onSelect]);
// 6. Early returns (loading, error, empty states)
if (loading) {
return <div className={styles.skeleton} aria-busy="true" />;
}
if (error) {
return (
<div className={styles.error} role="alert">
Failed to load user
</div>
);
}
if (!user) {
return null;
}
// 7. Main render
return (
<article className={styles.card}>
<h3 className={styles.name}>{user.name}</h3>
<p className={styles.email}>{user.email}</p>
<div className={styles.actions}>
<Button onClick={handleToggle}>
{isExpanded ? 'Collapse' : 'Expand'}
</Button>
<Button variant="primary" onClick={handleSelect}>
Select
</Button>
</div>
{isExpanded && (
<div className={styles.details}>
<p>{user.bio}</p>
</div>
)}
</article>
);
};
Use CSS Modules with CSS variables for theming. No Tailwind.
/* styles/variables.css */
:root {
/* Colors */
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-secondary: #64748b;
--color-success: #16a34a;
--color-error: #dc2626;
--color-warning: #d97706;
/* Text */
--color-text: #1f2937;
--color-text-muted: #6b7280;
--color-text-inverse: #ffffff;
/* Backgrounds */
--color-bg: #ffffff;
--color-bg-secondary: #f3f4f6;
--color-bg-elevated: #ffffff;
/* Borders */
--color-border: #e5e7eb;
--color-border-focus: #2563eb;
/* Spacing scale */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Border radius */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
}
/* Dark theme */
[data-theme='dark'] {
--color-text: #f9fafb;
--color-text-muted: #9ca3af;
--color-bg: #111827;
--color-bg-secondary: #1f2937;
--color-bg-elevated: #374151;
--color-border: #374151;
}
/* styles/globals.css */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
color: var(--color-text);
background-color: var(--color-bg);
line-height: 1.5;
}
/* Focus styles for accessibility */
:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
/* Buttons */
button {
cursor: pointer;
font-family: inherit;
font-size: inherit;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Form elements */
input,
textarea,
select {
font-family: inherit;
font-size: inherit;
}
/* styles/utilities.css */
/* Layout */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: var(--space-2); }
.gap-4 { gap: var(--space-4); }
/* Spacing */
.m-0 { margin: 0; }
.p-4 { padding: var(--space-4); }
.mt-4 { margin-top: var(--space-4); }
.mb-4 { margin-bottom: var(--space-4); }
/* Text */
.text-center { text-align: center; }
.text-muted { color: var(--color-text-muted); }
.font-medium { font-weight: 500; }
.font-bold { font-weight: 700; }
/* Visibility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* components/Button/Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-weight: 500;
border: 1px solid transparent;
border-radius: var(--radius-md);
transition: background-color var(--transition-fast),
border-color var(--transition-fast);
}
.button:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
/* Variants */
.primary {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.primary:hover:not(:disabled) {
background-color: var(--color-primary-hover);
}
.secondary {
background-color: transparent;
color: var(--color-text);
border-color: var(--color-border);
}
.secondary:hover:not(:disabled) {
background-color: var(--color-bg-secondary);
}
/* Sizes */
.small {
padding: var(--space-1) var(--space-2);
font-size: 0.875rem;
}
.large {
padding: var(--space-3) var(--space-6);
font-size: 1.125rem;
}
/* Mobile-first responsive design */
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--space-4);
}
.grid {
display: grid;
gap: var(--space-4);
grid-template-columns: 1fr;
}
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Stack on mobile, row on desktop */
.responsive-row {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
@media (min-width: 768px) {
.responsive-row {
flex-direction: row;
}
}
Extract reusable logic into custom hooks.
import { useState, useEffect, useCallback } from 'react';
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
export function useAsync<T>(
asyncFn: () => Promise<T>,
deps: unknown[] = []
): AsyncState<T> & { refetch: () => void } {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null,
});
const execute = useCallback(() => {
let cancelled = false;
setState((s) => ({ ...s, loading: true, error: null }));
asyncFn()
.then((data) => {
if (!cancelled) {
setState({ data, loading: false, error: null });
}
})
.catch((error) => {
if (!cancelled) {
setState({ data: null, loading: false, error });
}
});
return () => {
cancelled = true;
};
}, deps);
useEffect(() => {
return execute();
}, [execute]);
return { ...state, refetch: execute };
}
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
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;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch {
// Storage full or unavailable
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
import { useState, useEffect } from 'react';
export function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
Wrap feature sections with error boundaries.
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props.onError?.(error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
return (
this.props.fallback || (
<div role="alert" className="error-boundary">
<h2>Something went wrong</h2>
<p>Please try refreshing the page.</p>
</div>
)
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary
fallback={<ErrorFallback />}
onError={(error) => logToService(error)}
>
<Dashboard />
</ErrorBoundary>
);
}
import { useState, useId, type FormEvent } from 'react';
import styles from './LoginForm.module.css';
interface FormErrors {
email?: string;
password?: string;
}
export function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
const [errors, setErrors] = useState<FormErrors>({});
const emailId = useId();
const passwordId = useId();
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const newErrors: FormErrors = {};
if (!formData.get('email')) {
newErrors.email = 'Email is required';
}
if (!formData.get('password')) {
newErrors.password = 'Password is required';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
onSubmit(formData);
};
return (
<form
onSubmit={handleSubmit}
className={styles.form}
aria-labelledby="login-heading"
noValidate
>
<h2 id="login-heading">Login</h2>
<div className={styles.field}>
<label htmlFor={emailId}>
Email <span aria-label="required">*</span>
</label>
<input
id={emailId}
type="email"
name="email"
autoComplete="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? `${emailId}-error` : undefined}
required
/>
{errors.email && (
<span id={`${emailId}-error`} className={styles.error} role="alert">
{errors.email}
</span>
)}
</div>
<div className={styles.field}>
<label htmlFor={passwordId}>
Password <span aria-label="required">*</span>
</label>
<input
id={passwordId}
type="password"
name="password"
autoComplete="current-password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? `${passwordId}-error` : undefined}
required
/>
{errors.password && (
<span id={`${passwordId}-error`} className={styles.error} role="alert">
{errors.password}
</span>
)}
</div>
<button type="submit" className={styles.submit}>
Login
</button>
</form>
);
}
import { useState, useRef, useCallback, type KeyboardEvent } from 'react';
import styles from './Dropdown.module.css';
interface DropdownProps {
options: string[];
value: string;
onChange: (value: string) => void;
label: string;
}
export function Dropdown({ options, value, onChange, label }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const [focusedIndex, setFocusedIndex] = useState(0);
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLUListElement>(null);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setFocusedIndex((prev) =>
prev < options.length - 1 ? prev + 1 : 0
);
}
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex((prev) =>
prev > 0 ? prev - 1 : options.length - 1
);
break;
case 'Enter':
case ' ':
e.preventDefault();
if (isOpen) {
onChange(options[focusedIndex]);
setIsOpen(false);
buttonRef.current?.focus();
} else {
setIsOpen(true);
}
break;
case 'Escape':
setIsOpen(false);
buttonRef.current?.focus();
break;
case 'Tab':
setIsOpen(false);
break;
}
},
[isOpen, focusedIndex, options, onChange]
);
return (
<div className={styles.dropdown}>
<span id="dropdown-label" className="sr-only">
{label}
</span>
<button
ref={buttonRef}
type="button"
className={styles.trigger}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby="dropdown-label"
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
>
{value || 'Select option'}
</button>
{isOpen && (
<ul
ref={menuRef}
role="listbox"
className={styles.menu}
aria-labelledby="dropdown-label"
onKeyDown={handleKeyDown}
>
{options.map((option, index) => (
<li
key={option}
role="option"
aria-selected={option === value}
className={`${styles.option} ${
index === focusedIndex ? styles.focused : ''
}`}
onClick={() => {
onChange(option);
setIsOpen(false);
buttonRef.current?.focus();
}}
>
{option}
</li>
))}
</ul>
)}
</div>
);
}
// Bad: No cleanup for subscription
useEffect(() => {
const subscription = api.subscribe(handleUpdate);
}, []);
// Good: Cleanup prevents memory leaks
useEffect(() => {
const subscription = api.subscribe(handleUpdate);
return () => {
subscription.unsubscribe();
};
}, []);
// Bad: Missing dependency causes stale closure
useEffect(() => {
fetchData(userId);
}, []);
// Good: All dependencies listed
useEffect(() => {
fetchData(userId);
}, [userId]);
// Bad: Passing props through many levels
function App() {
const [user, setUser] = useState(null);
return <Layout user={user} setUser={setUser} />;
}
function Layout({ user, setUser }) {
return <Sidebar user={user} setUser={setUser} />;
}
// Good: Use context for widely shared state
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Layout />
</UserContext.Provider>
);
}
// Bad: Hard to maintain, no hover states, no theming
<button style={{
padding: '8px 16px',
backgroundColor: '#2563eb',
color: 'white'
}}>
Submit
</button>
// Good: CSS Module with theme variables
<button className={styles.primaryButton}>
Submit
</button>
// Bad: Unhandled error crashes entire app
function App() {
return <Dashboard />;
}
// Good: Error boundary contains failures
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Dashboard />
</ErrorBoundary>
);
}
// Bad: No feedback while loading
function UserList() {
const { data } = useFetch('/api/users');
return <ul>{data?.map(user => <li>{user.name}</li>)}</ul>;
}
// Good: Show loading indicator
function UserList() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <Spinner aria-label="Loading users" />;
if (error) return <Alert type="error">{error.message}</Alert>;
if (!data?.length) return <EmptyState>No users found</EmptyState>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
// Bad: Index as key causes issues when list changes
{items.map((item, index) => <Item key={index} {...item} />)}
// Good: Stable unique identifier
{items.map(item => <Item key={item.id} {...item} />)}
// Bad: Bypassing React's rendering
useEffect(() => {
document.getElementById('title').textContent = title;
}, [title]);
// Good: Let React handle DOM updates
return <h1 id="title">{title}</h1>;
If any answer is no, revise the code until all criteria are satisfied.