Agent skill

react-component-patterns

React + TypeScript component design patterns — composition, props API design, compound components, controlled vs uncontrolled, and memoization decisions

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/react-component-patterns

SKILL.md

React Component Patterns

Overview

Reference guide for idiomatic React + TypeScript component design. Apply these patterns when building, reviewing, or refactoring React components to ensure type safety, composability, and performance.

Composition Over Inheritance

Always compose components rather than extending them. React has no use case for class inheritance beyond React.Component itself.

tsx
// GOOD: Composition via props and children
type CardProps = {
  children: React.ReactNode;
  variant?: "elevated" | "outlined";
};

function Card({ children, variant = "elevated" }: CardProps) {
  return <div className={`card card--${variant}`}>{children}</div>;
}

function UserCard({ user }: { user: User }) {
  return (
    <Card variant="elevated">
      <Avatar src={user.avatar} />
      <CardBody>
        <h3>{user.name}</h3>
        <p>{user.bio}</p>
      </CardBody>
    </Card>
  );
}

Props API Design

Discriminated Unions for Variant Props

Use discriminated unions when props change shape based on a type field. Never use type?: string with conditional props.

tsx
// GOOD: Discriminated union — TypeScript enforces valid combinations
type ButtonProps =
  | { variant: "link"; href: string; onClick?: never }
  | { variant: "button"; onClick: () => void; href?: never }
  | { variant: "submit"; form: string; onClick?: never; href?: never };

function Button(props: ButtonProps) {
  switch (props.variant) {
    case "link":
      return <a href={props.href}>Link</a>;
    case "button":
      return <button onClick={props.onClick}>Click</button>;
    case "submit":
      return <button type="submit" form={props.form}>Submit</button>;
  }
}

Children Patterns

Use children for content projection. Use render props only when the child needs data from the parent.

tsx
// Content projection — simple, preferred
type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
};

// Render prop — when child needs parent data
type ListProps<T> = {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
};

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// Usage
<List
  items={users}
  renderItem={(user) => <UserRow user={user} />}
  keyExtractor={(user) => user.id}
/>

Polymorphic as Prop

Allow consumers to change the rendered element while preserving type safety.

tsx
type BoxProps<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<C>, "as" | "children">;

function Box<C extends React.ElementType = "div">({
  as,
  children,
  ...rest
}: BoxProps<C>) {
  const Component = as || "div";
  return <Component {...rest}>{children}</Component>;
}

// Usage — href is type-checked because as="a"
<Box as="a" href="/about">About</Box>
// Type error: href not valid on button
<Box as="button" href="/about">Nope</Box>

Compound Components

Use compound components when a group of components share implicit state. Expose a clean API via dot notation.

tsx
type TabsContextType = {
  activeTab: string;
  setActiveTab: (id: string) => void;
};

const TabsContext = createContext<TabsContextType | null>(null);

function useTabsContext() {
  const ctx = useContext(TabsContext);
  if (!ctx) throw new Error("Tabs compound components must be used within <Tabs>");
  return ctx;
}

function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
}

function TabList({ children }: { children: React.ReactNode }) {
  return <div role="tablist">{children}</div>;
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const { activeTab, setActiveTab } = useTabsContext();
  return (
    <button
      role="tab"
      aria-selected={activeTab === id}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
  const { activeTab } = useTabsContext();
  if (activeTab !== id) return null;
  return <div role="tabpanel">{children}</div>;
}

// Attach sub-components for dot notation
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
<Tabs defaultTab="profile">
  <Tabs.List>
    <Tabs.Tab id="profile">Profile</Tabs.Tab>
    <Tabs.Tab id="settings">Settings</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel id="profile">Profile content</Tabs.Panel>
  <Tabs.Panel id="settings">Settings content</Tabs.Panel>
</Tabs>

Controlled vs Uncontrolled

Support both patterns by detecting whether the value prop is provided.

tsx
type InputProps = {
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
};

function Input({ value: controlledValue, defaultValue = "", onChange }: InputProps) {
  const [internalValue, setInternalValue] = useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const value = isControlled ? controlledValue : internalValue;

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const next = e.target.value;
    if (!isControlled) setInternalValue(next);
    onChange?.(next);
  };

  return <input value={value} onChange={handleChange} />;
}

Memoization Decision Tree

When to use React.memo

Use React.memo only when you have measured a performance problem. Default: do not memo.

Should I use React.memo?
├── Is the component re-rendering frequently with the same props?
│   ├── YES → Does the render involve expensive computation or deep trees?
│   │   ├── YES → Use React.memo ✓
│   │   └── NO → Probably not worth it
│   └── NO → Don't use React.memo
└── Is the component a leaf node receiving only primitive props?
    └── YES, and parent re-renders often → React.memo is cheap here ✓
tsx
// JUSTIFIED: Expensive list item, parent re-renders on every keystroke
const ExpensiveRow = React.memo(function ExpensiveRow({ data }: { data: RowData }) {
  const processed = heavyComputation(data);
  return <tr>{/* ... */}</tr>;
});

useMemo — for expensive derived values

tsx
// GOOD: Expensive filter/sort on large dataset
const filtered = useMemo(
  () => items.filter(matchesSearch).sort(byDate),
  [items, matchesSearch, byDate]
);

// BAD: Trivial computation — overhead of useMemo > savings
const fullName = useMemo(() => `${first} ${last}`, [first, last]);
// Just compute it:
const fullName = `${first} ${last}`;

useCallback — for stable function references

Only needed when passing callbacks to memoized children or as effect dependencies.

tsx
// GOOD: onClick passed to a React.memo child
const handleClick = useCallback((id: string) => {
  setSelected(id);
}, []);

// BAD: No memoized consumer, useCallback is wasted
const handleClick = useCallback(() => {
  console.log("clicked");
}, []);
// Just use a plain function:
const handleClick = () => console.log("clicked");

Forwarding Refs

Use forwardRef for reusable primitive components that wrap DOM elements.

tsx
type InputProps = React.ComponentPropsWithoutRef<"input"> & {
  label: string;
  error?: string;
};

const TextField = forwardRef<HTMLInputElement, InputProps>(
  function TextField({ label, error, ...props }, ref) {
    const id = useId();
    return (
      <div>
        <label htmlFor={id}>{label}</label>
        <input ref={ref} id={id} aria-invalid={!!error} {...props} />
        {error && <span role="alert">{error}</span>}
      </div>
    );
  }
);

Anti-patterns

Prop Drilling Beyond 2 Levels

If a prop passes through more than 2 intermediate components that do not use it, introduce context or composition.

tsx
// BAD: theme drilled through 3 levels
<App theme={theme}>
  <Layout theme={theme}>
    <Sidebar theme={theme}>
      <NavItem theme={theme} />  // Only NavItem uses it

// GOOD: Use context
const ThemeContext = createContext<Theme>(defaultTheme);
// Or compose: pass <NavItem> as children so App can inject theme directly

God Components

A component over 200 lines or with more than 5 state variables is too large. Extract sub-components or custom hooks.

useEffect for Derived State

Never sync state from props via useEffect. Compute during render.

tsx
// BAD
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${first} ${last}`);
}, [first, last]);

// GOOD — compute during render
const fullName = `${first} ${last}`;

Boolean Prop Explosion

More than 2 boolean props usually means you need a discriminated union or a variant prop.

tsx
// BAD: 2^4 = 16 possible states, most invalid
<Button primary large outlined disabled />

// GOOD: variant constrains valid states
<Button variant="primary" size="lg" disabled />

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results