Tutorial React JavaScript Frontend Design Patterns

Free React Advanced Patterns Cheat Sheet Online — Interactive Reference for Developers

· 22 min read

React is not a framework that rewards memorizing API signatures. It rewards understanding patterns. Two developers can write functionally identical UIs using entirely different approaches — one with a tangle of useEffect hooks and prop drilling, the other with clean composition, custom hooks, and state colocation. The difference is not talent. It is pattern literacy. React advanced patterns are the vocabulary that separates experienced developers from beginners, and they are what this guide is built to teach.

This article is a deep-dive companion to our free interactive React Advanced Patterns Cheat Sheet. We cover the patterns that working React developers use daily: react render props, react higher order components, react compound components, react custom hooks, react state reducer, react provider pattern, react forward ref, react use imperative handle, react use transition, react use deferred value, react optimistic updates, react server actions, react error boundaries, react portals, react suspense patterns, react concurrent patterns, and react server components patterns. Each section includes production-ready code, practical guidance on when to apply the pattern, and common pitfalls to avoid. If you want to reference patterns quickly while coding, keep the cheat sheet open in another tab.

Why React Patterns Matter

React's API surface is intentionally small. The framework gives you components, hooks, and a reconciliation engine — then steps back. What you build with those primitives is up to you. This flexibility is powerful but also dangerous. Without patterns, teams invent ad-hoc solutions to the same problems, leading to inconsistent codebases, duplicated logic, and components that are impossible to reuse.

Patterns solve this by providing tested, repeatable solutions to common problems. They are not rules. They are shared vocabulary. When a developer says "let's use compound components for this tabs UI," everyone on the team knows what that means. When someone proposes a state reducer for a complex form, the team understands the tradeoffs without a whiteboard session. Patterns compress experience into reusable structure.

The patterns in this guide span the full React lifecycle. Some are classic — render props and HOCs have been around since the early days. Some are modern — React 18 concurrent features and React 19 Server Components represent the current frontier. All of them are patterns you will encounter in production codebases, open-source libraries, and technical interviews. Mastering them makes you a more effective React developer regardless of which version you are shipping.

Component Composition Patterns

Composition is React's core superpower. The ability to nest components, pass elements as children, and build complex UIs from simple pieces is why React won the frontend wars. But basic composition — parent renders children — is only the beginning. Advanced composition patterns give you precise control over what gets rendered, how data flows, and how components communicate without tight coupling.

Render Props

The render props pattern passes a function as a prop (or child) that returns JSX. The parent component calls this function with data, letting the consumer decide how to render it. This decouples data fetching or state management from presentation.

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

  useEffect(() => {
    const handleMouseMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return render(position);
}

// Usage
function App() {
  return (
    <MouseTracker render={({ x, y }) => (
      <p>Mouse is at {x}, {y}</p>
    )} />
  );
}

Use render props when you need to share behavior without dictating UI. They are explicit, easy to test, and avoid prop name collisions. The downside is nesting — multiple render props can create callback hell. In modern React, custom hooks have largely replaced render props for logic sharing, but render props still excel when the shared logic needs to wrap JSX with context or error boundaries.

Higher Order Components (HOC)

A Higher Order Component is a function that takes a component and returns a new component with additional props, state, or behavior. HOCs were the primary pattern for cross-cutting concerns before hooks existed.

function withAuth(WrappedComponent) {
  return function WithAuthComponent(props) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      authService.getCurrentUser()
        .then(setUser)
        .finally(() => setLoading(false));
    }, []);

    if (loading) return <Spinner />;
    if (!user) return <LoginRedirect />;

    return <WrappedComponent {...props} user={user} />;
  };
}

// Usage
const Dashboard = withAuth(function Dashboard({ user }) {
  return <h1>Welcome, {user.name}</h1>;
});

HOCs are powerful for injecting data, handling loading states, and adding behavior across many components. Their main drawbacks are prop name collisions, difficulty debugging the component stack, and the "wrapper hell" of deeply nested component trees. Today, prefer custom hooks for logic sharing and HOCs only when you need to wrap components with providers or error boundaries.

Compound Components

Compound components are a set of components that work together to form a complete UI. They share implicit state through React Context and communicate behind the scenes, giving the consumer a declarative API.

const TabsContext = createContext();

function Tabs({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ index, children }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  return (
    <button
      role="tab"
      aria-selected={activeIndex === index}
      className={activeIndex === index ? 'tab active' : 'tab'}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, children }) {
  const { activeIndex } = useContext(TabsContext);
  if (index !== activeIndex) return null;
  return <div role="tabpanel" className="tab-panel">{children}</div>;
}

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
function App() {
  return (
    <Tabs defaultIndex={0}>
      <Tabs.List>
        <Tabs.Tab index={0}>Account</Tabs.Tab>
        <Tabs.Tab index={1}>Security</Tabs.Tab>
        <Tabs.Tab index={2}>Notifications</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panel index={0}><AccountSettings /></Tabs.Panel>
      <Tabs.Panel index={1}><SecuritySettings /></Tabs.Panel>
      <Tabs.Panel index={2}><NotificationSettings /></Tabs.Panel>
    </Tabs>
  );
}

Compound components shine when building reusable UI primitives — tabs, accordions, dropdowns, and form controls. They give consumers flexibility over layout and styling while enforcing correct behavior internally. The tradeoff is increased internal complexity: you must manage context, validate child relationships, and handle edge cases like missing required children.

Children as Function

Children as function is a variant of render props where the child is a function instead of JSX. It is functionally identical to render props but uses the children prop slot, which some developers find more ergonomic.

function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return children({ data, error, loading });
}

// Usage
function UserProfile({ userId }) {
  return (
    <DataFetcher url={`/api/users/${userId}`}>
      {({ data, error, loading }) => {
        if (loading) return <Skeleton />;
        if (error) return <ErrorMessage error={error} />;
        return <UserCard user={data} />;
      }}
    </DataFetcher>
  );
}

This pattern is common in data fetching libraries and form libraries. It gives consumers full control over loading, error, and success states. Like render props, it can lead to deep nesting when composed heavily.

Polymorphic Components

Polymorphic components change their rendered element type via a prop, typically called as. This lets a single component render as a button, a link, or any other element while preserving its styles and behavior.

function PolymorphicButton({ as: Component = 'button', children, ...props }) {
  return (
    <Component
      className="btn"
      {...props}
    >
      {children}
    </Component>
  );
}

// Usage
<PolymorphicButton onClick={handleClick}>Click me</PolymorphicButton>
<PolymorphicButton as="a" href="/dashboard">Go to Dashboard</PolymorphicButton>
<PolymorphicButton as={Link} to="/profile">Profile</PolymorphicButton>

Polymorphic components are essential for design systems. A Button should be able to render as a native button, a link, or a React Router Link without duplicating styles. The complexity comes from TypeScript typing — correctly typing the as prop so that props are validated against the chosen element type requires advanced generic types.

Controlled vs Uncontrolled Components

Every form input in React can be managed in two ways: controlled, where React owns the state, or uncontrolled, where the DOM owns the state. Understanding when to use each, and how to combine them, is fundamental to React form architecture.

Controlled Components

In a controlled component, the input's value is driven by React state. Every keystroke triggers a re-render, giving you full control over validation, formatting, and derived state.

function ControlledForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    setError(value.includes('@') ? '' : 'Invalid email');
  };

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        placeholder="Enter email"
      />
      {error && <span className="error">{error}</span>}
    </form>
  );
}

Use controlled components when you need real-time validation, dynamic formatting, conditional UI based on input state, or synchronization with external state. The cost is more re-renders, which can matter for high-frequency inputs or large forms.

Uncontrolled Components with Ref

Uncontrolled components let the DOM manage the input state. You read values when needed — typically on submit — using refs.

function UncontrolledForm() {
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const email = emailRef.current.value;
    console.log('Submitted:', email);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" ref={emailRef} placeholder="Enter email" />
      <button type="submit">Submit</button>
    </form>
  );
}

Use uncontrolled components for simple forms where you only need values at submit time, when integrating with non-React form libraries, or when avoiding re-renders on every keystroke is critical. The downside is less control: you cannot validate in real time or disable the submit button based on input state without converting to controlled.

Key Reset Pattern

Changing a component's key forces React to unmount and remount it, resetting all internal state. This is a powerful technique for resetting uncontrolled components or forms.

function ResettableForm() {
  const [formKey, setFormKey] = useState(0);

  const handleReset = () => {
    setFormKey(k => k + 1);
  };

  return (
    <div>
      <UncontrolledForm key={formKey} />
      <button onClick={handleReset}>Reset Form</button>
    </div>
  );
}

This pattern is simpler than manually resetting every field in a controlled form. Use it when you need a complete reset to initial state.

Mixed Mode and the Controller Pattern

Real-world forms often mix controlled and uncontrolled inputs. The Controller pattern wraps uncontrolled inputs to make them behave like controlled ones, bridging the gap.

function Controller({ value, onChange, children }) {
  const [internalValue, setInternalValue] = useState(value);

  useEffect(() => {
    setInternalValue(value);
  }, [value]);

  const handleChange = (newValue) => {
    setInternalValue(newValue);
    onChange(newValue);
  };

  return children({ value: internalValue, onChange: handleChange });
}

// Usage with a third-party date picker
function DateField({ value, onChange }) {
  return (
    <Controller value={value} onChange={onChange}>
      {({ value, onChange }) => (
        <ThirdPartyDatePicker
          selected={value}
          onSelect={onChange}
        />
      )}
    </Controller>
  );
}

Libraries like React Hook Form use this pattern extensively to wrap uncontrolled inputs while providing a controlled API to the consumer.

State Management Patterns

State is the hardest problem in UI development. Where you place state, how you update it, and how components access it determines your application's performance, testability, and maintainability. These patterns give you a systematic approach to state architecture.

State Reducer Pattern

The state reducer pattern gives consumers full control over state transitions by accepting a reducer function as a prop. Instead of exposing individual state setters, the component calls the consumer's reducer with actions.

function useToggle({ initialOn = false, reducer = (state, action) => state } = {}) {
  const [on, setOn] = useState(initialOn);

  const dispatch = (action) => {
    const newState = reducer({ on }, action);
    setOn(newState.on);
  };

  const toggle = () => dispatch({ type: 'toggle' });
  const reset = () => dispatch({ type: 'reset', initialOn });

  return { on, toggle, reset, dispatch };
}

// Default behavior
function DefaultToggle() {
  const { on, toggle } = useToggle();
  return <button onClick={toggle}>{on ? 'On' : 'Off'}</button>;
}

// Custom reducer prevents toggling off during loading
function CustomToggle() {
  const { on, toggle } = useToggle({
    reducer(state, action) {
      if (action.type === 'toggle' && isLoading) return state;
      return { on: !state.on };
    }
  });
  return <button onClick={toggle}>{on ? 'On' : 'Off'}</button>;
}

This pattern is used in libraries like Downshift and React Table. It enables infinite customization without infinite props. The consumer can override any state transition while the component handles the rest.

Provider / Context Pattern

The Provider pattern uses React Context to share state across a component tree without prop drilling. It is the standard pattern for themes, authentication, and localization.

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  };

  const value = useMemo(() => ({ theme, toggleTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

// Usage
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button className={`btn btn-${theme}`} onClick={toggleTheme}>
      Toggle Theme
    </button>
  );
}

Use Context for low-frequency updates like themes and auth. For high-frequency state, Context causes every consumer to re-render on every change. Split state and dispatch into separate contexts, or use a specialized state library.

State Colocation

State colocation means keeping state as close as possible to the components that use it. The higher state lives in the tree, the larger the re-render blast radius when it changes.

// Before: Form state in Page causes Header, Sidebar, Footer to re-render
function Page() {
  const [formData, setFormData] = useState({});
  return (
    <>
      <Header />
      <Sidebar />
      <MainForm formData={formData} setFormData={setFormData} />
      <Footer />
    </>
  );
}

// After: State lives inside MainForm; siblings do not re-render
function Page() {
  return (
    <>
      <Header />
      <Sidebar />
      <MainForm />
      <Footer />
    </>
  );
}

Push state down before lifting it up. Only lift state when multiple siblings genuinely need synchronized access to it.

Lifting State Up

When two or more components need to share state, lift it to their nearest common ancestor. This is the canonical React pattern for shared state.

function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <CounterDisplay count={count} />
      <CounterControls count={count} setCount={setCount} />
    </div>
  );
}

function CounterDisplay({ count }) {
  return <p>Count: {count}</p>;
}

function CounterControls({ count, setCount }) {
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={() => setCount(c => c - 1)}>Decrement</button>
    </div>
  );
}

Lifting state up is simple and effective for small component trees. For deeply nested trees, it leads to prop drilling. In those cases, combine with Context or a state management library.

Derived State

Derived state is computed from existing state or props during render. It should not be stored in useState because that creates a synchronization problem.

// Bad: derived state requires manual syncing
function UserList({ users, filter }) {
  const [filteredUsers, setFilteredUsers] = useState(users);

  useEffect(() => {
    setFilteredUsers(users.filter(u => u.name.includes(filter)));
  }, [users, filter]);

  return <List items={filteredUsers} />;
}

// Good: derive during render
function UserList({ users, filter }) {
  const filteredUsers = useMemo(() =>
    users.filter(u => u.name.includes(filter)),
    [users, filter]
  );
  return <List items={filteredUsers} />;
}

Always derive values during render when possible. Use useMemo only when the computation is expensive. Never copy props into state unless you genuinely need a local editable copy.

URL as State

The URL is the most underutilized state container in web applications. It is shareable, bookmarkable, survives refreshes, and integrates with browser history.

function ProductFilters() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'name';

  const setCategory = (cat) => {
    setSearchParams(prev => {
      prev.set('category', cat);
      return prev;
    });
  };

  return (
    <div>
      <select value={category} onChange={e => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
    </div>
  );
}

Use URL state for filter states, pagination, search queries, and modal visibility. It makes your application stateful without JavaScript state management.

Custom Hooks Library

Custom hooks are React's answer to logic reuse. They let you extract component logic into reusable functions without changing your component hierarchy. A well-designed custom hook library is the difference between a codebase full of duplicated useEffect patterns and one where complex behavior is composed from simple, tested primitives.

useToggle

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setOn = useCallback(() => setValue(true), []);
  const setOff = useCallback(() => setValue(false), []);
  return { value, toggle, setOn, setOff };
}

// Usage
function FavoriteButton() {
  const { value: isFavorite, toggle } = useToggle(false);
  return (
    <button onClick={toggle}>
      {isFavorite ? '★' : '☆'}
    </button>
  );
}

useDebounce

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      searchAPI(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

useLocalStorage

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>Theme: {theme}</button>;
}

useFetch

function useFetch(url) {
  const [state, setState] = useState({ data: null, error: null, loading: true });

  useEffect(() => {
    let cancelled = false;
    setState({ data: null, error: null, loading: true });

    fetch(url)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) setState({ data, error: null, loading: false });
      })
      .catch(error => {
        if (!cancelled) setState({ data: null, error, loading: false });
      });

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

  return state;
}

// Usage
function UserProfile({ userId }) {
  const { data, error, loading } = useFetch(`/api/users/${userId}`);
  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  return <UserCard user={data} />;
}

useOnClickOutside

function useOnClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// Usage
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef();
  useOnClickOutside(ref, () => setIsOpen(false));

  return (
    <div ref={ref}>
      <button onClick={() => setIsOpen(!isOpen)}>Menu</button>
      {isOpen && <div className="dropdown-menu">...</div>}
    </div>
  );
}

useMediaQuery

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) setMatches(media.matches);

    const listener = (e) => setMatches(e.matches);
    media.addEventListener('change', listener);
    return () => media.removeEventListener('change', listener);
  }, [matches, query]);

  return matches;
}

// Usage
function ResponsiveNav() {
  const isDesktop = useMediaQuery('(min-width: 768px)');
  return isDesktop ? <DesktopNav /> : <MobileNav />;
}

Module and Component Architecture

How you organize files and structure components has a bigger impact on maintainability than any individual pattern. Good architecture makes features easy to find, changes localized, and onboarding fast.

Presentational / Container Pattern

This classic pattern separates components that know about state and data (containers) from components that only know about props and rendering (presentational).

// Presentational: pure, reusable, no side effects
function UserCard({ user, onEdit }) {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
}

// Container: knows about data fetching and state
function UserCardContainer({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  const handleEdit = () => {
    navigate(`/users/${userId}/edit`);
  };

  if (loading) return <Skeleton />;
  return <UserCard user={user} onEdit={handleEdit} />;
}

This pattern is less necessary with hooks — a custom useUser hook can replace the container entirely — but the separation of concerns remains valuable. Presentational components are your design system. Containers are your application logic.

Feature-Based Folder Structure

Instead of organizing by file type (components, hooks, utils), organize by feature. All code related to a feature lives together.

src/
  features/
    auth/
      components/
        LoginForm.jsx
        SignupForm.jsx
      hooks/
        useAuth.js
        useLogin.js
      services/
        authAPI.js
      context/
        AuthContext.jsx
      index.js
    dashboard/
      components/
        StatsWidget.jsx
        ActivityFeed.jsx
      hooks/
        useDashboardData.js
      services/
        dashboardAPI.js
      index.js
  shared/
    components/
      Button.jsx
      Modal.jsx
    hooks/
      useLocalStorage.js
    utils/
      formatDate.js

Feature-based folders scale better than type-based folders. When you need to modify authentication, every relevant file is in one place. Shared code lives in a separate directory to avoid duplication.

Barrel Exports

Barrel files (index.js) centralize exports from a directory, making imports cleaner.

// features/auth/index.js
export { AuthProvider, useAuth } from './context/AuthContext';
export { LoginForm } from './components/LoginForm';
export { SignupForm } from './components/SignupForm';
export { useLogin } from './hooks/useLogin';

// Usage elsewhere
import { LoginForm, useAuth } from '../features/auth';

Barrel exports reduce import path complexity but can interfere with tree shaking if not configured correctly. Ensure your bundler supports barrel file optimization.

Lazy Loading and Code Splitting

Split your bundle into chunks that load on demand. Route-based splitting is the most common pattern.

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Place Suspense boundaries strategically. A single boundary at the root means the entire app shows a spinner. Multiple boundaries let sections render independently.

React 18+ Concurrent Patterns

React 18 introduced concurrent rendering, allowing React to prepare multiple versions of the UI simultaneously. This enables new patterns for keeping interfaces responsive during heavy updates.

useTransition

useTransition marks a state update as non-urgent. React keeps the current UI interactive while preparing the new state in the background.

function FilterableList({ items }) {
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);

  function handleChange(e) {
    const value = e.target.value;
    setFilter(value); // urgent: update input immediately

    startTransition(() => {
      setFilteredItems(items.filter(item => item.name.includes(value))); // non-urgent
    });
  }

  return (
    <>
      <input value={filter} onChange={handleChange} />
      {isPending && <Spinner />}
      <ItemList items={filteredItems} />
    </>
  );
}

Use useTransition for heavy state updates triggered by user input: filtering large lists, switching tabs, or updating complex visualizations.

useDeferredValue

useDeferredValue returns a deferred version of a value that lags behind the original. It is conceptually similar to debouncing but integrated with React's rendering schedule.

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  return (
    <>
      <SearchInput value={query} onChange={setQuery} />
      <SearchResults query={deferredQuery} />
      {query !== deferredQuery && <span>Loading...</span>}
    </>
  );
}

useDeferredValue is ideal for search UIs where the input must stay responsive while results compute. Unlike useTransition, it does not require wrapping the state update — you pass the deferred value directly to the slow component.

Suspense Boundaries

React 18 stabilizes Suspense for data fetching. Wrap components that suspend in Suspense boundaries to show fallback UI while data resolves.

function ProfilePage() {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <ProfileData />
    </Suspense>
  );
}

function ProfileData() {
  const user = useUserData(); // throws promise if not ready
  return <ProfileCard user={user} />;
}

Automatic Batching

React 18 automatically batches all state updates, regardless of where they originate. Multiple setState calls in a timeout, promise, or native event handler are batched into a single render.

// React 18: both setStates batched into a single render
function handleClick() {
  fetch('/api/user').then(response => {
    setUser(response.user);
    setNotifications(response.notifications);
  });
}

Error Handling Patterns

Errors in React propagate up the component tree until they hit an error boundary. Without error boundaries, a single thrown error crashes your entire application.

Error Boundaries

Error boundaries are class components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo);
  }

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

// Usage
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Router />
    </ErrorBoundary>
  );
}

Error boundaries do not catch errors in event handlers, async code, server-side rendering, or errors thrown in the error boundary itself. For those, use try/catch and manual error state.

react-error-boundary

The react-error-boundary library provides a modern, hook-based API for error boundaries without writing class components.

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => window.location.reload()}>
      <Router />
    </ErrorBoundary>
  );
}

Reset Patterns

After an error, users need a way to recover. Provide a reset button that clears the error state and remounts the failed component tree.

function ResettableErrorBoundary({ children }) {
  const [key, setKey] = useState(0);

  return (
    <ErrorBoundary
      key={key}
      fallback={
        <div>
          <p>An error occurred.</p>
          <button onClick={() => setKey(k => k + 1)}>Retry</button>
        </div>
      }
    >
      {children}
    </ErrorBoundary>
  );
}

Advanced Ref Patterns

Refs provide escape hatches from React's declarative model. They let you access DOM nodes, store mutable values, and expose imperative APIs to parent components.

forwardRef

forwardRef lets a component receive a ref from its parent and pass it to a DOM node or child component.

const FancyInput = forwardRef(function FancyInput(props, ref) {
  return <input ref={ref} className="fancy-input" {...props} />;
});

// Usage
function Parent() {
  const inputRef = useRef();
  return (
    <>
      <FancyInput ref={inputRef} placeholder="Enter name" />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </>
  );
}

Use forwardRef when building reusable components that need to expose their underlying DOM node — inputs, buttons, and custom form controls.

useImperativeHandle

useImperativeHandle customizes the instance value exposed when a parent uses ref on your component. It lets you expose a clean API instead of the entire DOM node.

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ''; },
    getValue: () => inputRef.current.value,
  }), []);

  return <input ref={inputRef} className="fancy-input" {...props} />;
});

// Usage
function Parent() {
  const inputRef = useRef();

  const handleSubmit = () => {
    console.log(inputRef.current.getValue());
    inputRef.current.clear();
  };

  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

Use useImperativeHandle sparingly. It breaks React's declarative model. Prefer props and callbacks for parent-child communication. Reserve imperative handles for complex components like media players, maps, and canvas editors.

Merge Refs

When a component receives multiple refs — one from the parent, one from a library — you need to merge them so all refs point to the same node.

function mergeRefs(...refs) {
  return (node) => {
    refs.forEach(ref => {
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref != null) {
        ref.current = node;
      }
    });
  };
}

// Usage
const FancyInput = forwardRef(function FancyInput(props, forwardedRef) {
  const localRef = useRef();

  return <input ref={mergeRefs(localRef, forwardedRef)} {...props} />;
});

Callback Refs

Callback refs give you a function that React calls with the DOM node. They are useful for measuring elements or integrating with non-React libraries.

function MeasuredComponent() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <div ref={measuredRef}>Measure my height</div>
      <p>Height: {height}px</p>
    </>
  );
}

React 19 and Server Components

React Server Components (RSC) represent a fundamental shift in React architecture. They render exclusively on the server, can access backend resources directly, and never ship JavaScript to the client.

React Server Components Architecture

RSCs are the default in modern React frameworks like Next.js App Router. They work alongside Client Components, which handle interactivity and browser APIs.

// Server Component — runs on server, no JS shipped to client
async function ProductList() {
  const products = await db.products.findMany({ take: 10 });

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          <ProductCard product={product} />
        </li>
      ))}
    </ul>
  );
}

// Client Component — interactive, ships JS
'use client';

function ProductCard({ product }) {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      <h3>{product.name}</h3>
      {isHovered && <QuickView product={product} />}
    </div>
  );
}

Use Server Components for data fetching, accessing backend resources, and rendering static content. Use Client Components for interactivity, browser APIs, and state. The boundary is marked with the 'use client' directive.

Server Actions

Server Actions let you call server-side functions directly from Client Components. They replace API routes for form submissions and mutations.

// Server Action
async function createTodo(formData) {
  'use server';

  const title = formData.get('title');
  await db.todos.create({ data: { title, completed: false } });
  revalidatePath('/todos');
}

// Client Component
'use client';

function TodoForm() {
  return (
    <form action={createTodo}>
      <input name="title" placeholder="New todo" required />
      <button type="submit">Add</button>
    </form>
  );
}

Server Actions eliminate boilerplate API routes and type-safe client-server communication. They are ideal for forms, mutations, and any user action that modifies server state.

useOptimistic

useOptimistic lets you show the final state optimistically while a server action is in flight. If the action fails, React automatically reverts to the previous state.

'use client';

import { useOptimistic } from 'react';

function TodoList({ initialTodos }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, newTodo) => [...state, { ...newTodo, sending: true }]
  );

  async function handleSubmit(formData) {
    const title = formData.get('title');
    addOptimisticTodo({ id: Date.now(), title, completed: false });
    await createTodo(formData);
  }

  return (
    <>
      <form action={handleSubmit}>
        <input name="title" placeholder="New todo" />
        <button type="submit">Add</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
            {todo.title}
          </li>
        ))}
      </ul>
    </>
  );
}

useFormStatus

useFormStatus provides the submission status of a parent form. It is useful for showing loading states on submit buttons.

'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

function ContactForm() {
  return (
    <form action={sendMessage}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <SubmitButton />
    </form>
  );
}

Streaming

React 18+ supports streaming SSR, sending HTML to the client as it is generated. Suspense boundaries define chunks that can stream independently.

function ProductPage() {
  return (
    <>
      <ProductHeader /> {/* streams immediately */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails /> {/* streams when data resolves */}
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews /> {/* streams independently */}
      </Suspense>
    </>
  );
}

Conclusion

React advanced patterns are not theoretical exercises. They are the tools working developers use to build maintainable, performant, and reusable UIs. From react composition patterns like render props and compound components to react state patterns like state reducer and URL as state, from react custom hooks that extract reusable logic to react concurrent patterns that keep UIs responsive under load — these patterns form the vocabulary of professional React development.

Our free interactive React Advanced Patterns Cheat Sheet puts every pattern from this article at your fingertips. Each entry includes a concise explanation, a production-ready code example, and practical guidance on when to use it. Search by keyword, filter by category, and copy any snippet with one click. The tool runs entirely in your browser with no server interaction.

Mastering these patterns takes time and practice. Start with the ones that solve problems you face today. If you are building a design system, study compound components and polymorphic components. If you are optimizing a large form, explore controlled and uncontrolled patterns. If you are adopting React 18 or 19, prioritize concurrent features and Server Components. The cheat sheet is designed to be a living reference — bookmark it and return whenever you need a refresher.

Explore related references to deepen your React expertise:

Patterns are not rules. They are starting points. The best React developers know when to apply a pattern and when to break it. Build your pattern vocabulary, practice applying them, and develop the judgment to choose the right tool for each problem. That is how you write React code that scales.

Found this useful? Check out our free developer tools or browse more articles.