react
npx skills add https://github.com/tgautier/dotfiles --skill react
Agent 安装分布
Skill 文档
React Development
React 19 patterns for production applications. Covers the framework layer â hooks, state, data fetching, error handling, and API client integration.
For TypeScript strictness, testing, and build tooling, see /typescript. For component design, form UX, and accessibility, see /ux-design. For CSS and responsive patterns, see /css-responsive.
1. React 19 Patterns
Actions and useActionState
Replace manual useState + try/catch + setLoading with built-in action support:
function CreateForm() {
const [state, submitAction, isPending] = useActionState(
async (_prev: FormState, formData: FormData) => {
const result = await createResource(formData);
if (!result.ok) return { error: result.error };
return { error: null };
},
{ error: null },
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Create</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
useOptimistic for perceived performance
Apply optimistic updates on mutations â React reverts automatically on error:
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(current, newItem: Item) => [...current, newItem],
);
use() hook
Reads promises and context inside conditionals and loops (unlike other hooks):
function ResourceDetail({ resourcePromise }: { resourcePromise: Promise<Resource> }) {
const resource = use(resourcePromise); // suspends until resolved
return <h1>{resource.name}</h1>;
}
Server Components
Move data-fetching and heavy logic to Server Components. Only add "use client" to components that need interactivity (event handlers, state, effects). Server Components reduce client bundle by 30-50%.
2. Data Fetching
Route loaders as the primary mechanism
Loaders run before render, eliminating loading spinners on navigation:
export async function loader({ params }: LoaderFunctionArgs) {
const resource = await api.getResource(params.id);
return { resource };
}
export default function ResourcePage() {
const { resource } = useLoaderData<typeof loader>();
return <ResourceDetail resource={resource} />;
}
TanStack Query for cache + background refetch
Layer TanStack Query on top of loaders for stale-while-revalidate, retry logic, and cache management:
// Query key factory â consistent cache keys
const resourceKeys = {
all: ["resources"] as const,
list: (filters: Filters) => [...resourceKeys.all, "list", filters] as const,
detail: (id: string) => [...resourceKeys.all, "detail", id] as const,
};
// In loader: prefetch and seed the cache
export async function loader({ params }: LoaderFunctionArgs) {
await queryClient.ensureQueryData(resourceQueryOptions(params.id));
return null;
}
// In component: read from cache + subscribe to updates
function ResourceDetail() {
const { data } = useSuspenseQuery(resourceQueryOptions(useParams().id!));
return <h1>{data.name}</h1>;
}
Key rules
- Set a global
staleTime(e.g., 60 seconds) â the default of 0 causes excessive refetches - Never copy server data into
useStateâ let TanStack Query own it - Use
useSuspenseQuerywith Suspense boundaries instead of checkingisLoading/isError - Wrap route segments in
Suspense+ErrorBoundarypairs:
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Skeleton />}>
<ResourceList />
</Suspense>
</ErrorBoundary>
3. Error Handling
Layered error boundaries
- Root boundary: Catches catastrophic errors, shows a full-page error screen
- Route boundary: Catches loader/action errors per route (framework-provided
errorElement) - Feature boundary: Wraps individual widgets so a single failure doesn’t take down the page
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => queryClient.invalidateQueries()}
resetKeys={[resourceId]}
>
<ResourceDetail />
</ErrorBoundary>
What error boundaries do NOT catch
- Errors in event handlers (use try/catch)
- Async errors outside React rendering (handle in promise chains or TanStack Query)
- Errors in the error boundary itself
Retry pattern
Offer a “Try again” button that calls resetErrorBoundary(). For API calls, TanStack Query provides built-in exponential backoff via the retry option.
Toast notifications
Use for non-blocking errors (e.g., “Failed to save, retrying…”). Libraries: sonner, react-hot-toast. Never use toasts as the sole error indicator for form validation.
4. State Management
Decision framework
Choose the right tool for each type of state:
| State type | Tool | Example |
|---|---|---|
| Server data | TanStack Query / route loaders | Fetched resources, lists |
| URL state | useSearchParams |
Filters, pagination, search |
| Form state | React Hook Form / useActionState |
Input values, validation |
| Global UI state | Zustand | Theme, sidebar, preferences |
| Fine-grained reactivity | Jotai | Dependent atoms, derived state |
| Complex workflows | XState | Multi-step forms, state machines |
Key rules
- Never copy server data into
useStateâ let TanStack Query or loaders own it - URL state is the most underused location â filters, sort order, and pagination belong in the URL
- Use selectors to prevent unnecessary re-renders:
// WRONG: subscribes to entire store
const store = useStore();
// CORRECT: subscribes only to theme
const theme = useStore((s) => s.theme);
When Redux is appropriate
Only when: you need time-travel debugging, have very complex state interactions, or have a large team that benefits from strict conventions. For most applications, Zustand or Jotai are simpler and sufficient.
5. API Client Patterns
Generated clients from OpenAPI
Use @hey-api/openapi-ts (actively maintained) over openapi-typescript-codegen (unmaintained):
- Generates type-safe fetch functions from OpenAPI spec
- Supports Zod schema generation for runtime validation
- TanStack Query plugin for automatic query hooks
Request cancellation
const controller = new AbortController();
const response = await fetch("/api/resources", { signal: controller.signal });
// On unmount or navigation:
controller.abort();
AbortController instances are single-use â create a new one per request. Handle AbortError separately from network errors.
Type-safe error mapping
Map API error responses to discriminated unions:
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: { code: "validation"; fields: Record<string, string> } }
| { ok: false; error: { code: "not_found" } }
| { ok: false; error: { code: "server_error" } };
Retry with exponential backoff
Retry only on 429 and 503. Respect Retry-After headers. TanStack Query’s default retry option retries all failures â customize it:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (failureCount >= 3) return false;
const status = error instanceof ApiError ? error.status : 0;
return status === 429 || status === 503;
},
},
},
});
6. Hooks & Composition
When to extract a custom hook
Extract a hook when:
- Logic is shared between 2+ components
- A component has complex state management that obscures its rendering intent
- You need to test the logic independently from the UI
Don’t extract when:
- The logic is used in only one component and is simple
- The “hook” would just be a thin wrapper around a single
useState
Naming conventions
useprefix is mandatory (React enforces this)- Name describes what the hook provides, not how:
useAssets()notuseFetchAssets() - Return an object for 3+ values, a tuple for 1-2:
const [value, setValue] = useToggle()
Hook composition
Build complex hooks from simpler ones:
function useAssetFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const filters = useMemo(() => parseFilters(searchParams), [searchParams]);
const setFilter = useCallback((key: string, value: string) => {
setSearchParams(prev => { prev.set(key, value); return prev; });
}, [setSearchParams]);
return { filters, setFilter };
}
function useAssets() {
const { filters } = useAssetFilters();
return useSuspenseQuery(assetQueryOptions(filters));
}
Hook testing
Test hooks with renderHook from Testing Library:
import { renderHook, act } from "@testing-library/react";
test("useToggle toggles value", () => {
const { result } = renderHook(() => useToggle(false));
expect(result.current[0]).toBe(false);
act(() => result.current[1]());
expect(result.current[0]).toBe(true);
});
Wrap hooks that need providers (Router, Query Client) in a wrapper:
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<MemoryRouter>{children}</MemoryRouter>
</QueryClientProvider>
);
const { result } = renderHook(() => useAssets(), { wrapper });
7. Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
Copy server data into useState |
Stale data, double source of truth | Let TanStack Query / loaders own it |
useEffect for data fetching |
Waterfalls, race conditions, no caching | Route loaders or TanStack Query |
Manual useMemo/useCallback everywhere |
Noise, premature optimization | React Compiler handles memoization |
| Prop drilling through 4+ levels | Fragile, hard to refactor | Context, composition, or state library |
useEffect for derived state |
Extra render cycle, stale values | Compute during render with useMemo |
| One giant Context for all state | Every consumer re-renders on any change | Split into focused contexts or use Zustand |
useLayoutEffect without SSR guard |
Server warning, runs as useEffect on server |
useEffect or useIsomorphicLayoutEffect |
| Catching errors in event handlers with boundaries | Boundaries only catch render/lifecycle errors | try/catch in the handler |
Fetching in useEffect with no cleanup |
Race conditions, memory leaks | AbortController or TanStack Query |
| Storing form values in global state | Unnecessary complexity | React Hook Form or useActionState |
Cross-references
/typescriptâ TypeScript strictness, testing (Vitest/Playwright), modules, build tooling/ux-designâ component API design, form UX, accessibility/css-responsiveâ responsive rendering, Tailwind CSS patterns