From frontend-react
React component patterns for building robust, composable UI — function components, hooks rules, Server vs Client Components, composition strategies, props design, and performance optimizations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-react:react-componentsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Always use function components. Class components are legacy and must not be written for new code.
Always use function components. Class components are legacy and must not be written for new code.
// DO
export function UserCard({ user }: UserCardProps) {
return <div>{user.name}</div>;
}
// DON'T
class UserCard extends React.Component { ... }
Next.js App Router defaults every component to a Server Component unless opted out.
| Capability | Server Component | Client Component |
|---|---|---|
| async/await at top level | yes | no |
| Access DB / filesystem directly | yes | no |
| useState / useReducer | no | yes |
| useEffect / event handlers | no | yes |
| Browser APIs | no | yes |
| Imported by Server Component | yes | yes (but becomes a boundary) |
Rules:
"use client" as deep as possible.children or a prop)."use client" at the top of any file that uses hooks, event handlers, or browser APIs.// server-component.tsx (default — no directive needed)
import { db } from "@/lib/db";
import { ClientWidget } from "./ClientWidget";
export async function ServerPage() {
const items = await db.items.findMany();
return <ClientWidget initialItems={items} />;
}
// ClientWidget.tsx
"use client";
import { useState } from "react";
export function ClientWidget({ initialItems }: Props) {
const [items, setItems] = useState(initialItems);
return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}
use.// DO: consistent hook call order
function MyComponent({ userId }: Props) {
const [count, setCount] = useState(0);
const user = useUser(userId); // custom hook
// ...
}
// DON'T: conditional hook
function MyComponent({ show }: Props) {
if (show) {
const [val, setVal] = useState(0); // violates rules of hooks
}
}
Prefer children for generic container components.
interface CardProps {
children: React.ReactNode;
className?: string;
}
export function Card({ children, className }: CardProps) {
return <div className={cn("card", className)}>{children}</div>;
}
Use when the parent needs to control the rendering logic and pass data back to the child.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
export function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
// Usage
<List items={users} renderItem={(u) => <span>{u.name}</span>} />
Use for component families that share implicit state (e.g., Select, Accordion, Tabs).
const TabsContext = React.createContext<TabsContextValue | null>(null);
export function Tabs({ children, defaultValue }: TabsProps) {
const [active, setActive] = useState(defaultValue);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
}
Tabs.List = function TabList({ children }: { children: React.ReactNode }) {
return <div role="tablist">{children}</div>;
};
Tabs.Tab = function Tab({ value, children }: TabProps) {
const ctx = useContext(TabsContext)!;
return (
<button role="tab" aria-selected={ctx.active === value} onClick={() => ctx.setActive(value)}>
{children}
</button>
);
};
Define a dedicated props interface for every component. Use interface over type for component props unless union/intersection types are needed.
interface ButtonProps {
variant?: "primary" | "secondary" | "ghost";
size?: "sm" | "md" | "lg";
disabled?: boolean;
loading?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}
Destructure props directly in the function signature with inline defaults.
export function Button({
variant = "primary",
size = "md",
disabled = false,
loading = false,
onClick,
children,
}: ButtonProps) {
// ...
}
Avoid blind prop spreading ({...props}) on DOM elements as it can pass unknown attributes. Use explicit forwarding.
// DON'T
export function Input({ label, ...props }) {
return <input {...props} />; // may forward non-DOM props
}
// DO
export function Input({ label, className, value, onChange, ...inputProps }: InputProps) {
return (
<label>
{label}
<input value={value} onChange={onChange} className={className} {...inputProps} />
</label>
);
}
Use React.forwardRef when a component must expose its underlying DOM node to a parent.
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
}
export const LabeledInput = React.forwardRef<HTMLInputElement, InputProps>(
function LabeledInput({ label, ...props }, ref) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
);
React error boundaries catch rendering errors in a subtree. They must be class components (the only remaining valid use case) or use a library like react-error-boundary.
Prefer react-error-boundary for all new code:
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function FeatureSection() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { /* clear state */ }}>
<FeatureContent />
</ErrorBoundary>
);
}
In Next.js App Router, error.tsx files serve as error boundaries for route segments.
Wrap async data or lazy-loaded components in <Suspense> with a meaningful fallback.
import { Suspense, lazy } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
export function DashboardPage() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
);
}
loading.tsx automatically wraps its segment in a Suspense boundary.React.memo: wrap a component when it re-renders frequently with the same props and its render cost is measurable.useMemo: memoize expensive computations (sorting, filtering large arrays) or when the value is a reference passed as a prop to a memoized component.useCallback: stabilize a callback reference passed as a prop to a memoized component or as a dependency in useEffect.memo by default — measure first.useMemo for cheap computations (string concatenation, simple arithmetic).useCallback for functions that are recreated cheaply and not passed as props.// DO: memoize expensive filter
const filteredItems = useMemo(
() => items.filter(i => i.category === selectedCategory),
[items, selectedCategory]
);
// DO: stabilize callback passed to memoized child
const handleSelect = useCallback((id: string) => {
dispatch({ type: "SELECT", id });
}, [dispatch]);
// DON'T: pointless memoization
const label = useMemo(() => `Hello, ${name}`, [name]); // trivially cheap
| Do | Don't |
|---|---|
| Function components | Class components (except error boundary base) |
"use client" only where needed | Apply "use client" to entire feature dirs |
| Typed props interface | Untyped props: any |
| Compose via children / render props | Deep prop drilling more than 2-3 levels |
react-error-boundary | Unhandled render errors in production |
| Measure before memoizing | Premature memo everywhere |
npx claudepluginhub gagandeepp/software-agent-teams --plugin frontend-reactGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.