·11 min read

React Patterns I Use Often

Introduction

As you continue developing with React, you start to notice patterns that you use repeatedly. In this article, I've compiled the patterns and techniques I frequently use in my daily development.

Component Design

Compound Components Pattern

A pattern for grouping related components and providing a flexible API.

// Usage
<Card>
  <Card.Header>
    <Card.Title>Title</Card.Title>
  </Card.Header>
  <Card.Content>Content</Card.Content>
  <Card.Footer>Footer</Card.Footer>
</Card>

Implementation:

function Card({ children }: { children: React.ReactNode }) {
  return <div className="rounded-lg border bg-card p-4">{children}</div>;
}
 
function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="mb-4">{children}</div>;
}
 
function CardTitle({ children }: { children: React.ReactNode }) {
  return <h3 className="text-lg font-semibold">{children}</h3>;
}
 
// Combine as namespace
Card.Header = CardHeader;
Card.Title = CardTitle;
Card.Content = CardContent;
Card.Footer = CardFooter;
 
export { Card };

Render Props Pattern

A pattern for sharing logic while delegating rendering to the caller.

type MousePosition = { x: number; y: number };
 
function MouseTracker({ children }: { children: (position: MousePosition) => React.ReactNode }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener("mousemove", handleMouseMove);
    return () => window.removeEventListener("mousemove", handleMouseMove);
  }, []);
 
  return <>{children(position)}</>;
}
 
// Usage
<MouseTracker>
  {({ x, y }) => (
    <p>
      Mouse position: ({x}, {y})
    </p>
  )}
</MouseTracker>;

Custom Hooks

useLocalStorage

A state management hook that syncs with local storage.

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
 
  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };
 
  return [storedValue, setValue] as const;
}
 
// Usage
const [theme, setTheme] = useLocalStorage("theme", "light");

useDebounce

A hook that delays value updates. Useful for search inputs.

function useDebounce<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;
}
 
// Usage
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 300);
 
useEffect(() => {
  // API call
  search(debouncedSearchTerm);
}, [debouncedSearchTerm]);

useMediaQuery

A hook that monitors media query state.

function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);
 
  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);
 
    const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
    media.addEventListener("change", listener);
    return () => media.removeEventListener("change", listener);
  }, [query]);
 
  return matches;
}
 
// Usage
const isMobile = useMediaQuery("(max-width: 768px)");

Performance Optimization

useMemo and useCallback

Memoization to prevent recalculation and recreation.

// Memoize expensive calculations
const expensiveResult = useMemo(() => {
  return items.filter((item) => item.active).map((item) => transform(item));
}, [items]);
 
// Memoize callbacks (when passing as props to child components)
const handleClick = useCallback((id: string) => {
  setSelected(id);
}, []);

Note: Don't memoize everything. Memoization itself has a cost, so only use it when truly necessary.

React.lazy and Suspense

Lazy loading components.

const HeavyComponent = lazy(() => import("./HeavyComponent"));
 
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

State Management Patterns

Reducer Pattern

Useful for managing complex state transitions.

type State = { count: number; step: number };
type Action = { type: "increment" } | { type: "decrement" } | { type: "setStep"; payload: number };
 
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + state.step };
    case "decrement":
      return { ...state, count: state.count - state.step };
    case "setStep":
      return { ...state, step: action.payload };
    default:
      return state;
  }
}
 
// Usage
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

Type-Safe Patterns

Discriminated Union

A pattern utilizing type narrowing.

type Result<T> =
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };
 
function DataDisplay({ result }: { result: Result<User[]> }) {
  switch (result.status) {
    case "loading":
      return <Spinner />;
    case "error":
      return <ErrorMessage error={result.error} />;
    case "success":
      return <UserList users={result.data} />;
  }
}

Generic Components

Reusable type-safe components.

type SelectProps<T> = {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
};
 
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={getValue(value)}
      onChange={(e) => {
        const selected = options.find((opt) => getValue(opt) === e.target.value);
        if (selected) onChange(selected);
      }}
    >
      {options.map((option) => (
        <option key={getValue(option)} value={getValue(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  );
}

Conclusion

It's important to use these patterns appropriately based on the situation. If a simple solution is sufficient, there's no need to force-fit a pattern.

Choose the right patterns while keeping code readability and maintainability in mind.