RE

Patterns

React · 8 entries

Custom Hooks

syntax
function useCustomHook(params) {
  // use built-in hooks inside
  return result;
}
example
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null ? JSON.parse(stored) : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

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

Note Custom hooks must start with 'use' so React can enforce the rules of hooks. They let you extract and share stateful logic between components without changing the component hierarchy.

Compound Components

syntax
function Parent({ children }) { ... }
Parent.Child = function Child(props) { ... };
example
const AccordionContext = createContext(null);

function Accordion({ children }) {
  const [openIndex, setOpenIndex] = useState(null);
  return (
    <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

function AccordionItem({ index, title, children }) {
  const { openIndex, setOpenIndex } = useContext(AccordionContext);
  const isOpen = openIndex === index;

  return (
    <div>
      <button onClick={() => setOpenIndex(isOpen ? null : index)}>
        {title} {isOpen ? '−' : '+'}
      </button>
      {isOpen && <div className="panel">{children}</div>}
    </div>
  );
}

Accordion.Item = AccordionItem;

// Usage
<Accordion>
  <Accordion.Item index={0} title="Section A">Content A</Accordion.Item>
  <Accordion.Item index={1} title="Section B">Content B</Accordion.Item>
</Accordion>

Note Compound components share implicit state via context, letting you create flexible APIs like <Select> + <Select.Option>. The parent manages state and children consume it without prop drilling.

Render Props

syntax
function DataProvider({ render }) {
  const data = useData();
  return render(data);
}
// or via children
<DataProvider>{(data) => <View data={data} />}</DataProvider>
example
function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

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

  return children(position);
}

// Usage
<MouseTracker>
  {({ x, y }) => (
    <div>
      Cursor is at ({x}, {y})
    </div>
  )}
</MouseTracker>

Note Render props are largely replaced by custom hooks, which are simpler. Still useful when you need to control what renders based on shared logic without creating a custom hook for every case.

Error Boundaries

syntax
// Error boundaries must be class components (as of React 19)
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, info) { logError(error, info); }
  render() {
    if (this.state.hasError) return <Fallback />;
    return this.props.children;
  }
}
example
class AppErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  componentDidCatch(error, errorInfo) {
    reportToService(error, errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-screen">
          <h1>Something went wrong</h1>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap sections of the UI
<AppErrorBoundary>
  <Dashboard />
</AppErrorBoundary>

Note Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the tree below them. They do NOT catch errors in event handlers, async code, or server-side rendering. Use try/catch for those.

Portals

syntax
import { createPortal } from 'react-dom';
createPortal(children, domNode);
example
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        <button className="close-btn" onClick={onClose}>X</button>
        {children}
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

// Usage
function App() {
  const [showModal, setShowModal] = useState(false);
  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open</button>
      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Title</h2>
        <p>Modal body content here.</p>
      </Modal>
    </div>
  );
}

Note Portals render children into a different DOM node outside the parent hierarchy, but events still bubble through the React tree (not the DOM tree). Perfect for modals, tooltips, and dropdowns that need to escape overflow:hidden or z-index contexts.

Suspense & Lazy Loading

syntax
const LazyComponent = React.lazy(() => import('./Component'));

<Suspense fallback={<Loading />}>
  <LazyComponent />
</Suspense>
example
import { lazy, Suspense, useState } from 'react';

const AdminPanel = lazy(() => import('./AdminPanel'));
const UserDashboard = lazy(() => import('./UserDashboard'));

function App({ role }) {
  return (
    <Suspense fallback={<div className="spinner">Loading...</div>}>
      {role === 'admin' ? <AdminPanel /> : <UserDashboard />}
    </Suspense>
  );
}

Note React.lazy only works with default exports. For named exports, create an intermediate module that re-exports as default. Suspense can also wrap components that use the use() hook to read promises in React 19.

Context + Reducer Pattern

syntax
const StateCtx = createContext(null);
const DispatchCtx = createContext(null);

function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, init);
  return (
    <StateCtx.Provider value={state}>
      <DispatchCtx.Provider value={dispatch}>
        {children}
      </DispatchCtx.Provider>
    </StateCtx.Provider>
  );
}
example
const TodoStateContext = createContext(null);
const TodoDispatchContext = createContext(null);

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: crypto.randomUUID(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(t =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    case 'DELETE':
      return state.filter(t => t.id !== action.id);
    default:
      throw new Error(`Unhandled: ${action.type}`);
  }
}

function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return (
    <TodoStateContext.Provider value={todos}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

function useTodos() {
  return useContext(TodoStateContext);
}
function useTodoDispatch() {
  return useContext(TodoDispatchContext);
}

Note This pattern provides Redux-like state management with built-in React APIs. Splitting state and dispatch into separate contexts prevents components that only dispatch actions from re-rendering when state changes.

Higher-Order Components (HOC)

syntax
function withFeature(WrappedComponent) {
  return function EnhancedComponent(props) {
    const data = useFeatureData();
    return <WrappedComponent {...props} data={data} />;
  };
}
example
function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();

    if (loading) return <p>Authenticating...</p>;
    if (!user) return <LoginRedirect />;

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

// Usage
const ProtectedDashboard = withAuth(Dashboard);

function App() {
  return <ProtectedDashboard showStats />;
}

Note HOCs were the primary code reuse pattern before hooks. Custom hooks are now preferred for most cases because they are simpler and avoid the wrapper component nesting. HOCs are still useful for cross-cutting concerns like auth guards.