Skip to content

React — Patterns

OWNER: floris, floor ALSO_USED_BY: alexander, tobias LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: React 19.x


Overview

Component architecture patterns for GE projects. Agents use this page when deciding how to structure components. Start with the decision tree, then follow the relevant pattern section.


Decision Tree — Component Architecture

Does the component render a list of similar items?
  YES → Composition with mapped children (Section 1)
  NO ↓

Does the component have multiple related sub-components sharing state?
  YES → Compound Component (Section 2)
  NO ↓

Does the component need to share stateful logic across unrelated components?
  YES → Custom Hook (Section 3)
  NO ↓

Does a parent need to control what a child renders with parent's data?
  YES → Render Prop (Section 4)
  NO ↓

Does the component need cross-cutting behavior (logging, auth, analytics)?
  YES → HOC if class-based integration, otherwise Custom Hook (Section 5)
  NO ↓

Does the component need to catch JS errors in its subtree?
  YES → Error Boundary (Section 6)
  NO ↓

Does the component need to render outside its DOM parent?
  YES → Portal (Section 7)
  NO ↓

Use plain component composition with props.

1. Composition — Props and Children

The default pattern. Prefer this unless a specific need drives another choice.

CHECK: component accepts children for flexible content slots CHECK: named slots use explicit props, not children inspection IF: component has more than 5 props THEN: group related props into a config object or split the component

export interface CardProps {
  header: React.ReactNode;
  footer?: React.ReactNode;
  children: React.ReactNode;
}

export function Card({ header, footer, children }: CardProps): React.ReactElement {
  return (
    <div className="rounded-lg border bg-card">
      <div className="border-b p-4">{header}</div>
      <div className="p-4">{children}</div>
      {footer && <div className="border-t p-4">{footer}</div>}
    </div>
  );
}

ANTI_PATTERN: inspecting children with React.Children.map to assign slots FIX: use explicit named props for slots (header, footer, sidebar)


2. Compound Components

Use when multiple sub-components share implicit state through a parent. Examples: Tabs, Accordion, Select, Dropdown, DataTable.

CHECK: parent provides context via React Context CHECK: custom hook wraps useContext with error if used outside provider CHECK: sub-components are attached as static properties on parent

// tabs-context.ts
interface TabsContextValue {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

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

function useTabs(): TabsContextValue {
  const context = useContext(TabsContext);
  if (!context) {
    throw new Error("useTabs must be used within <Tabs>");
  }
  return context;
}

// tabs.tsx
export function Tabs({ defaultTab, children }: TabsProps): React.ReactElement {
  const [activeTab, setActiveTab] = useState(defaultTab);
  const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
  return (
    <TabsContext.Provider value={value}>
      {children}
    </TabsContext.Provider>
  );
}

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

export function TabTrigger({ value, children }: TabTriggerProps): React.ReactElement {
  const { activeTab, setActiveTab } = useTabs();
  return (
    <button
      role="tab"
      aria-selected={activeTab === value}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}

export function TabContent({ value, children }: TabContentProps): React.ReactElement | null {
  const { activeTab } = useTabs();
  if (activeTab !== value) return null;
  return <div role="tabpanel">{children}</div>;
}

ANTI_PATTERN: using React.cloneElement to inject props into children FIX: use Context API — cloneElement breaks with fragments and wrappers

ANTI_PATTERN: compound components without error boundary on context access FIX: always throw descriptive error in the custom hook if context is null


3. Custom Hooks

Extract reusable stateful logic that is independent of UI.

CHECK: hook name starts with use CHECK: hook returns a well-typed object or tuple CHECK: hook has no JSX — it returns data and callbacks only CHECK: hook has a dedicated test file IF: hook wraps an external API THEN: return { data, error, isLoading } shape for consistency

// use-debounced-value.ts
export function useDebouncedValue<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// use-media-query.ts
export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const mql = window.matchMedia(query);
    setMatches(mql.matches);
    const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
    mql.addEventListener("change", handler);
    return () => mql.removeEventListener("change", handler);
  }, [query]);

  return matches;
}

ANTI_PATTERN: hook that does two unrelated things FIX: split into two focused hooks — one concern per hook

ANTI_PATTERN: hook with useEffect that fetches data FIX: use TanStack Query or SWR for data fetching — custom hooks for non-server state only


4. Render Props

Use when a component needs to delegate rendering to its consumer. Less common in modern React but valid for headless UI libraries.

CHECK: render prop is typed as a function returning ReactNode IF: you control both provider and consumer THEN: prefer custom hook over render prop — cleaner API IF: you are building a reusable library component THEN: render prop gives consumers maximum flexibility

interface MouseTrackerProps {
  render: (position: { x: number; y: number }) => React.ReactNode;
}

export function MouseTracker({ render }: MouseTrackerProps): React.ReactElement {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = useCallback((e: React.MouseEvent) => {
    setPosition({ x: e.clientX, y: e.clientY });
  }, []);

  return <div onMouseMove={handleMouseMove}>{render(position)}</div>;
}

ANTI_PATTERN: deeply nested render props (render prop hell) FIX: convert to custom hooks — compose hooks in the consumer component


5. Higher-Order Components (HOCs)

Rarely needed in modern React. Custom hooks cover most use cases.

CHECK: HOC is only used for class-based library integration IF: wrapping a component with auth/analytics/error behavior THEN: prefer a custom hook + wrapper component over HOC IF: integrating with a library that requires HOC pattern THEN: type the HOC correctly with generics

// Only use when a library demands it
export function withAuth<P extends object>(
  Component: React.ComponentType<P>
): React.FC<P> {
  return function AuthenticatedComponent(props: P) {
    const { user, isLoading } = useAuth();
    if (isLoading) return <Skeleton />;
    if (!user) redirect("/login");
    return <Component {...props} />;
  };
}

ANTI_PATTERN: stacking multiple HOCs on one component FIX: use custom hooks composed inside the component

ANTI_PATTERN: HOC that hides what props it injects FIX: make injected props explicit in the type signature


6. Error Boundaries

Required for every route segment and async data boundary.

CHECK: every page has an error boundary (Next.js error.tsx) CHECK: error boundary provides recovery action (retry button) CHECK: error boundary logs to monitoring before rendering fallback IF: a section of the page can fail independently THEN: wrap it in a granular error boundary

"use client";

export default function ErrorBoundary({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}): React.ReactElement {
  useEffect(() => {
    console.error("Caught error:", error);
    // Send to monitoring service
  }, [error]);

  return (
    <div role="alert" className="p-4 text-destructive">
      <h2>Something went wrong</h2>
      <button onClick={reset} className="mt-2 underline">
        Try again
      </button>
    </div>
  );
}

ANTI_PATTERN: error boundary that swallows errors silently FIX: always log the error before rendering fallback

ANTI_PATTERN: single top-level error boundary for entire app FIX: place error boundaries at each route segment and around risky async sections


7. Portals

Render UI outside the parent DOM hierarchy. Use for modals, toasts, tooltips, dropdowns.

CHECK: portal renders into a dedicated DOM container CHECK: portal still respects React event bubbling IF: using shadcn/ui Dialog, Popover, or Tooltip THEN: portal is handled internally — do not add another IF: building custom overlay THEN: use createPortal with a stable container ref

"use client";

import { createPortal } from "react-dom";

export function Modal({ open, children }: ModalProps): React.ReactElement | null {
  if (!open) return null;
  return createPortal(
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
      {children}
    </div>,
    document.body
  );
}

ANTI_PATTERN: portal that creates a new DOM element on every render FIX: use a stable container — query or create once, reuse


GE-Specific Conventions

shadcn/ui First

CHECK: before building any UI primitive, check if shadcn/ui has it IF: shadcn/ui has the component THEN: use it — customize via Tailwind, not by forking the source IF: shadcn/ui does not have it THEN: build using the same patterns (Radix primitives + Tailwind + cva)

Component Size Limits

CHECK: component file is under 200 lines IF: component exceeds 200 lines THEN: extract sub-components or custom hooks IF: component has more than 3 useEffect calls THEN: it is doing too much — split it

Prop Drilling Threshold

CHECK: props pass through at most 2 intermediate components IF: prop passes through 3+ layers without being used THEN: use Context or Zustand


Cross-References

READ_ALSO: wiki/docs/stack/react/index.md READ_ALSO: wiki/docs/stack/react/state-management.md READ_ALSO: wiki/docs/stack/react/pitfalls.md READ_ALSO: wiki/docs/stack/react/checklist.md