| name | react-data |
| description | Load when writing, reviewing, or debugging React/TypeScript code โ components, hooks, data fetching with TanStack Query, forms, and state management. Covers the patterns Claude consistently gets wrong: useEffect for data fetching, missing loading/error states, Zod schema mismatches, and TypeScript anti-patterns.
|
React + TypeScript: What Claude Gets Wrong in Production Code
This skill covers React with TypeScript, TanStack Query v5, and Zod. If the project
uses a different data fetching library, note the differences.
Data Fetching: TanStack Query, Not useEffect
useEffect for data fetching causes race conditions, missing loading/error handling,
no caching, and double-fetching in Strict Mode. Use TanStack Query.
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch("/api/v1/orders")
.then(res => res.json())
.then(data => setOrders(data))
.finally(() => setLoading(false));
}, []);
import { useQuery } from "@tanstack/react-query";
function useOrders() {
return useQuery({
queryKey: ["orders"],
queryFn: () => api.get<Order[]>("/api/v1/orders"),
});
}
function OrderList() {
const { data: orders, isLoading, isError, error } = useOrders();
if (isLoading) return <Spinner />;
if (isError) return <ErrorMessage error={error} />;
return <ul>{orders.map(o => <OrderItem key={o.id} order={o} />)}</ul>;
}
Query Keys: Structured, Not Flat Strings
Query keys determine cache identity. Structured keys let you invalidate precisely.
useQuery({ queryKey: ["orders"] })
useQuery({ queryKey: ["user-orders"] })
useQuery({ queryKey: ["orders", "list"] })
useQuery({ queryKey: ["orders", "detail", orderId] })
useQuery({ queryKey: ["orders", "list", { status: "pending" }] })
queryClient.invalidateQueries({ queryKey: ["orders"] })
queryClient.invalidateQueries({ queryKey: ["orders", "detail", orderId] })
Store query key factories in a dedicated file:
export const orderKeys = {
all: ["orders"] as const,
lists: () => [...orderKeys.all, "list"] as const,
detail: (id: string) => [...orderKeys.all, "detail", id] as const,
};
Mutations: Optimistic Updates Done Right
const mutation = useMutation({
mutationFn: (id: string) => api.delete(`/orders/${id}`),
onSuccess: () => queryClient.refetchQueries({ queryKey: orderKeys.lists() }),
});
const mutation = useMutation({
mutationFn: (id: string) => api.delete(`/orders/${id}`),
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: orderKeys.lists() });
const previous = queryClient.getQueryData(orderKeys.lists());
queryClient.setQueryData(orderKeys.lists(), (old: Order[]) =>
old.filter(o => o.id !== id)
);
return { previous };
},
onError: (_err, _id, context) => {
queryClient.setQueryData(orderKeys.lists(), context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: orderKeys.lists() });
},
});
TypeScript: Types From Zod Schemas, Not Parallel Definitions
Don't define a TypeScript type and a Zod schema separately โ they drift.
Generate the type from the schema.
interface Order {
id: string;
total: number;
status: "pending" | "confirmed" | "shipped";
}
const OrderSchema = z.object({
id: z.string().uuid(),
total: z.number(),
status: z.enum(["pending", "confirmed", "shipped"]),
});
const OrderSchema = z.object({
id: z.string().uuid(),
total: z.number(),
status: z.enum(["pending", "confirmed", "shipped"]),
created_at: z.string().datetime(),
});
type Order = z.infer<typeof OrderSchema>;
async function fetchOrder(id: string): Promise<Order> {
const res = await fetch(`/api/v1/orders/${id}`);
const data = await res.json();
return OrderSchema.parse(data);
}
TypeScript Anti-Patterns
function processData(data: any) {
return data.items.map((item: any) => item.id);
}
const order = apiResponse as Order;
function processData(data: unknown) {
const parsed = OrderListSchema.parse(data);
return parsed.items.map(item => item.id);
}
const name = user!.profile!.name;
const name = user?.profile?.name ?? "Unknown";
Component Patterns: Avoid Prop Drilling
<Page user={user}>
<Layout user={user}>
<Sidebar user={user}>
<UserAvatar user={user} />
// RIGHT โ use context for cross-cutting concerns, or colocate data fetching
function UserAvatar() {
const { data: user } = useCurrentUser(); // fetch directly where needed
return <img src={user?.avatarUrl} />;
}
// For shared UI state (modal open, theme, etc.) โ context is appropriate
const ModalContext = createContext<ModalContextValue | null>(null);
Common Gotchas
Stale closures in useEffect: Always include all values used inside useEffect
in the dependency array. If a value changes but isn't in the array, the effect uses
the stale captured value.
useEffect(() => {
fetchOrders(userId);
}, []);
useEffect(() => {
fetchOrders(userId);
}, [userId]);
key prop on lists: React uses key to identify elements across renders.
Never use array index as key โ it causes incorrect reconciliation when items reorder.
Use a stable, unique ID (database ID, UUID).
useCallback/useMemo overuse: Don't wrap everything. Only memoize when
a value is used as a dependency of another hook, or when rendering is measurably
slow. Premature memoization adds complexity with no benefit.