D
Dev SOPKnowledge Base
Search
← All topics

React Advanced Patterns: Compound Components, Render Props, and Performance

Production React patterns including compound components, render props, custom hooks, memoization strategy, and React 18 concurrent features.

reactpatternsperformancehookstypescript
Agent trigger phrases: React compound components · render props · useOptimistic · React performance · useMemo vs useCallback · React 18 concurrent

Overview

Advanced React patterns for production applications — beyond the basics of useState and useEffect.

Compound Components Pattern

Exposes a family of related components that share implicit state through context:

// Context holds shared state
const TabsContext = createContext<TabsContextType | null>(null);

function Tabs({ children, defaultTab }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

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

// Attach as static properties
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

Usage: <Tabs defaultTab="a"><Tabs.Tab id="a">Profile</Tabs.Tab></Tabs>

Custom Hooks — Composition Over Inheritance

Extract stateful logic into reusable hooks:

function useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[]) {
  const [state, setState] = useState<{
    data: T | null;
    error: Error | null;
    loading: boolean;
  }>({ data: null, error: null, loading: true });

  useEffect(() => {
    setState(s => ({ ...s, loading: true }));
    asyncFn()
      .then(data => setState({ data, error: null, loading: false }))
      .catch(error => setState({ data: null, error, loading: false }));
  }, deps);

  return state;
}

Memoization Decision Matrix

| Situation | Use | Why | |-----------|-----|-----| | Expensive calculation | useMemo | Skip recalculation | | Callback passed to child component | useCallback | Stable reference | | Complex child that re-renders often | React.memo | Skip re-render | | Simple primitive value | Nothing | Memoization has overhead too |

Rule: Profile first. React.memo only pays off when the render is actually expensive or the component is deep in the tree.

React 18 Concurrent Features

useTransition — Non-Urgent Updates

const [isPending, startTransition] = useTransition();

function handleSearch(query: string) {
  setInputValue(query);        // Urgent: update input immediately
  startTransition(() => {
    setSearchResults(query);   // Non-urgent: can be interrupted
  });
}

useDeferredValue — Defer Derived State

const deferredQuery = useDeferredValue(query);
// deferredQuery lags behind query — prevents blocking the input
const results = useMemo(() => search(deferredQuery), [deferredQuery]);

useOptimistic — Instant UI Updates

"use client";
const [optimisticTodos, addOptimistic] = useOptimistic(
  todos,
  (state, newTodo: Todo) => [...state, newTodo]
);

async function addTodo(formData: FormData) {
  const todo = { id: crypto.randomUUID(), title: formData.get("title") as string };
  addOptimistic(todo);      // Instant UI update
  await createTodo(todo);   // Server action (actual save)
}

State Management Decision Matrix

| Scope | Tool | When | |-------|------|------| | Local UI state | useState | Form inputs, toggles, modals | | Complex local state | useReducer | State machine, many related fields | | Cross-component | Context | Theme, auth, locale (low-frequency updates) | | Server state | React Query / SWR | API data, caching, background refetch | | Global client state | Zustand | Complex app state, persisted preferences |

Performance Checklist

  • Keys on list items are stable IDs, never array indexes
  • useEffect dependencies are correct and complete
  • Client components are pushed as low as possible in the tree
  • Images use loading="lazy" or <Image> (Next.js)
  • Heavy libraries are dynamically imported: const Chart = dynamic(() => import('./Chart'))