React Component Patterns

Common React component patterns — composition, render props, HOCs, controlled forms, and error boundaries.

Component Composition

Build UIs from small, focused components that compose together.

function Card({ children }: { children: React.ReactNode }) {
  return <div className="card">{children}</div>;
}

function App() {
  return (
    <Card>
      <h2>Title</h2>
      <p>Content goes here.</p>
    </Card>
  );
}

Prop Patterns

Pattern Description
Optional props Use ? in TypeScript: variant?: "primary"
Default props Destructure with defaults: { variant = "primary" }
Spread props Pass rest: <button {...rest} />
Children children: React.ReactNode for composition
Render props Pass a function as a child or prop for flexible rendering

Controlled Components

Form elements whose value is driven by React state.

function SearchForm() {
  const [query, setQuery] = useState("");

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log(query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Compound Components

Related components that share implicit state through context.

const TabsContext = createContext<{
  activeTab: string;
  setActiveTab: (id: string) => void;
}>({ activeTab: "", setActiveTab: () => {} });

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

function TabList({ children }: { children: React.ReactNode }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ id, children }: { id: string; children: React.ReactNode }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={activeTab === id ? "active" : ""}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

// Usage
<Tabs defaultTab="overview">
  <TabList>
    <Tab id="overview">Overview</Tab>
    <Tab id="settings">Settings</Tab>
  </TabList>
</Tabs>

Error Boundaries

Catch rendering errors in a subtree. Must be a class component.

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback?: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error("ErrorBoundary caught:", error, info);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? <p>Something went wrong.</p>;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary fallback={<p>Failed to load.</p>}>
  <UnstableComponent />
</ErrorBoundary>

Data Fetching Pattern

Fetch data in an effect with loading and error states.

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function load() {
      try {
        setLoading(true);
        const data = await fetchUser(userId);
        if (!cancelled) setUser(data);
      } catch (err) {
        if (!cancelled) setError(err as Error);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    load();
    return () => { cancelled = true; };
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!user) return null;
  return <h1>{user.name}</h1>;
}

Key Rules

Rule Description
One component, one file Keep components in their own files
Named exports Prefer named exports over default exports
Hooks at top level Never call hooks inside loops, conditions, or nested functions
Keys for lists Always provide stable, unique keys when rendering lists
Immutable state Never mutate state directly — always create new objects/arrays
Lift state up Move shared state to the nearest common ancestor