Fossils⚛ïļ React PatternsPatterns & State Management Interview Questions
ðŸĢHatchlingReactPatternsHOCCustom HooksContextRedux

Patterns & State Management Interview Questions

HOCs, render props, custom hooks, Context vs Redux — the architecture questions that test your design thinking.

Patterns & State Management Interview Questions

These questions test whether you can design React applications, not just build features. Interviewers want to see that you understand the tradeoffs between different patterns and can pick the right tool for the job.


Q11: What Are Higher-Order Components (HOCs)?

Interview Question: "What is a Higher-Order Component? Give an example."

The Simple Explanation

Think of a HOC like a phone case. Your phone (the component) works fine on its own, but the case (the HOC) wraps around it and adds extra capabilities — protection, a card holder, a kickstand — without changing the phone itself.

A Higher-Order Component is a function that takes a component and returns a new, enhanced component with additional props or behavior.

HOC(OriginalComponent) → EnhancedComponent

The Classic Example: withAuth

function withAuth(WrappedComponent: React.ComponentType<any>) {
  return function AuthenticatedComponent(props: any) {
    const { user, isLoading } = useAuth();
 
    if (isLoading) return <LoadingSpinner />;
    if (!user) return <Navigate to="/login" />;
 
    return <WrappedComponent {...props} user={user} />;
  };
}
 
// Usage: wrap any component to require authentication
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);
 
// Now these redirect to login if not authenticated
<Route path="/dashboard" element={<ProtectedDashboard />} />
<Route path="/settings" element={<ProtectedSettings />} />

The Dashboard and Settings components don't know or care about authentication logic. The HOC handles it.

Another Example: withLogging

function withLogging(WrappedComponent: React.ComponentType<any>) {
  return function LoggedComponent(props: any) {
    useEffect(() => {
      console.log(`${WrappedComponent.name} mounted`);
      return () => console.log(`${WrappedComponent.name} unmounted`);
    }, []);
 
    return <WrappedComponent {...props} />;
  };
}

Why HOCs Are Less Common Now

HOCs were the dominant pattern before hooks. They have real downsides:

  • Wrapper hell — stacking HOCs creates deeply nested component trees (withAuth(withTheme(withLogging(MyComponent))))
  • Prop collisions — two HOCs might inject a prop with the same name
  • Hard to type — TypeScript generics for HOCs are complex
  • Invisible data flow — you can't easily see where props come from

Today, custom hooks solve the same problems more cleanly. But you'll still see HOCs in older codebases (React Router v5, Redux connect, etc.).

The One-Liner That Impresses

"A Higher-Order Component is a function that takes a component and returns an enhanced component — it's the decorator pattern applied to React. They were the primary abstraction for cross-cutting concerns before hooks, and while custom hooks have largely replaced them, you'll still encounter HOCs in older codebases and libraries like Redux's connect()."

Common Follow-Up Questions

"When would you still use a HOC over a custom hook?"

When you need to modify the component tree itself — like conditionally rendering a different component entirely (redirect to login, show error boundary). Hooks can't return early from rendering a different component.

"What's the convention for naming HOCs?"

The function is named withSomething (withAuth, withTheme) and the wrapped component's display name should be set for DevTools: EnhancedComponent.displayName = "withAuth(" + WrappedComponent.displayName + ")".

Red Flags in Your Answer

  • Not being able to write a simple HOC from scratch
  • Not mentioning the downsides (wrapper hell, prop collisions)
  • Not knowing that custom hooks are the modern alternative
  • Confusing HOCs with render props

Q12: What Are Render Props?

Interview Question: "Explain the render props pattern."

The Simple Explanation

Think of render props like a restaurant that lets you cook your own meal. The restaurant (the component) provides the kitchen, ingredients, and equipment (data and behavior), but you decide what dish to make (how to render it).

A render prop is a function prop that a component uses to know what to render. The component handles the logic, and the function you pass in handles the UI.

The Pattern

interface MouseTrackerProps {
  render: (position: { x: number; y: number }) => React.ReactNode;
}
 
function MouseTracker({ render }: MouseTrackerProps) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
 
  return <>{render(position)}</>;
}
 
// Usage: YOU decide how to display the mouse position
<MouseTracker render={({ x, y }) => (
  <div>Mouse is at ({x}, {y})</div>
)} />
 
<MouseTracker render={({ x, y }) => (
  <div style={{ position: 'absolute', left: x, top: y }}>
    👆 Cursor follower!
  </div>
)} />

The "Children as a Function" Variant

Instead of a prop called render, you can use children as the function:

function MouseTracker({ children }: { children: (pos: { x: number; y: number }) => React.ReactNode }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
 
  return <>{children(position)}</>;
}
 
// Looks cleaner
<MouseTracker>
  {({ x, y }) => <p>Position: {x}, {y}</p>}
</MouseTracker>

Why Render Props Are Less Common Now

Like HOCs, render props were the go-to before hooks. The same MouseTracker as a custom hook:

function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
 
  useEffect(() => {
    const handleMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);
 
  return position;
}
 
// Usage — much simpler
function CursorFollower() {
  const { x, y } = useMousePosition();
  return <div style={{ position: 'absolute', left: x, top: y }}>👆</div>;
}

Custom hooks are simpler, more composable, and don't create extra nesting.

The One-Liner That Impresses

"Render props invert control by letting the consumer decide what to render with data provided by the component — it's the strategy pattern for React UI. While custom hooks have replaced most render prop use cases, you'll still see the pattern in libraries like Formik, Downshift, and React Router's Route component."

Common Follow-Up Questions

"When would render props be better than a hook?"

When the component needs to provide both data AND a wrapper element (like a scroll container or a dropdown menu that manages its own positioning). The render prop component can wrap the rendered content in its own DOM structure.

Red Flags in Your Answer

  • Confusing render props with regular props
  • Not seeing how custom hooks replace most render prop use cases
  • Unable to write a render prop component from scratch
  • Not mentioning the "children as a function" variant

Q13: What Are Custom Hooks?

Interview Question: "How do custom hooks work? Build one for me."

The Simple Explanation

Think of a custom hook like a recipe. If you find yourself making the same dish in multiple components — measuring ingredients, mixing, baking — you write the recipe once and just follow it wherever you need it.

A custom hook is a function that starts with use and can call other hooks inside it. It lets you extract and share stateful logic between components without changing their structure.

Building a Real Custom Hook: useInfiniteScroll

Let's build something interviewers love — an infinite scroll hook:

interface UseInfiniteScrollOptions<T> {
  fetchPage: (page: number) => Promise<T[]>;
  threshold?: number;
}
 
interface UseInfiniteScrollResult<T> {
  items: T[];
  isLoading: boolean;
  error: Error | null;
  observerRef: (node: HTMLElement | null) => void;
}
 
function useInfiniteScroll<T>({
  fetchPage,
  threshold = 0.8,
}: UseInfiniteScrollOptions<T>): UseInfiniteScrollResult<T> {
  const [items, setItems] = useState<T[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
 
  const pageRef = useRef(1);
  const hasMoreRef = useRef(true);
  const isFetchingRef = useRef(false);
 
  const loadMore = useCallback(async () => {
    if (isFetchingRef.current || !hasMoreRef.current) return;
 
    isFetchingRef.current = true;
    setIsLoading(true);
 
    try {
      const newItems = await fetchPage(pageRef.current);
      if (newItems.length === 0) {
        hasMoreRef.current = false;
      } else {
        setItems(prev => [...prev, ...newItems]);
        pageRef.current += 1;
      }
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Fetch failed'));
    } finally {
      isFetchingRef.current = false;
      setIsLoading(false);
    }
  }, [fetchPage]);
 
  // Callback ref that sets up an IntersectionObserver on the sentinel element
  const observerRef = useCallback(
    (node: HTMLElement | null) => {
      if (!node) return;
 
      const observer = new IntersectionObserver(
        ([entry]) => {
          if (entry.isIntersecting) loadMore();
        },
        { threshold }
      );
      observer.observe(node);
 
      return () => observer.disconnect();
    },
    [loadMore, threshold]
  );
 
  // Load first page on mount
  useEffect(() => {
    loadMore();
  }, [loadMore]);
 
  return { items, isLoading, error, observerRef };
}

Usage in a component:

function ProductFeed() {
  const { items, isLoading, error, observerRef } = useInfiniteScroll<Product>({
    fetchPage: (page) => fetch(`/api/products?page=${page}`).then(r => r.json()),
  });
 
  return (
    <div>
      {items.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
      {/* Sentinel element — when it scrolls into view, load more */}
      <div ref={observerRef} />
      {isLoading && <Spinner />}
      {error && <ErrorMessage error={error} />}
    </div>
  );
}

The Rules of Custom Hooks

  1. Name starts with use — this tells React and the linter that hook rules apply
  2. Can call other hooks — useState, useEffect, useRef, other custom hooks
  3. Each component gets its own copy — two components using useInfiniteScroll get independent state
  4. Must follow hooks rules — no conditional calls, no loops, top-level only

Composition: Hooks That Use Hooks

The real power of custom hooks is composing them:

function useAuth() {
  const [user, setUser] = useLocalStorage<User | null>('auth_user', null);
  const isOnline = useNetworkStatus();
 
  const login = useCallback(async (credentials: Credentials) => {
    const user = await authAPI.login(credentials);
    setUser(user);
  }, [setUser]);
 
  const logout = useCallback(() => {
    setUser(null);
  }, [setUser]);
 
  return { user, isOnline, login, logout };
}

useAuth doesn't re-implement local storage or network detection — it composes existing hooks.

The One-Liner That Impresses

"Custom hooks are the React way to share stateful logic — each caller gets an independent copy of the state, but the logic is written once. They replaced HOCs and render props as the primary abstraction for cross-cutting concerns because they compose naturally, don't create wrapper nesting, and work seamlessly with TypeScript."

Common Follow-Up Questions

"How is a custom hook different from a regular function?"

A regular function can't call hooks. Custom hooks can call useState, useEffect, and other hooks because React recognizes functions starting with use as hooks and applies the rules of hooks to them.

"Should you always extract logic into a custom hook?"

No. Only extract when the logic is reused across components or when the component is getting too complex. Extracting a hook for logic used in a single place adds indirection without benefit.

Red Flags in Your Answer

  • Not knowing the naming convention (must start with use)
  • Thinking state is shared between components that use the same hook
  • Unable to build a custom hook from scratch
  • Not seeing custom hooks as the replacement for HOCs/render props

Q19: When Should You Use Context API vs Redux?

Interview Question: "When would you use React Context vs an external state library like Redux?"

The Simple Explanation

Think of it like communication in a company.

React Context is like a bulletin board in the break room. Great for announcements everyone needs to see (company theme, who's logged in, what language we're using). But if you update it every 5 minutes, everyone in the room is constantly distracted.

Redux (or Zustand/Jotai) is like a filing cabinet with individual drawers. Each person can subscribe to only the drawer they care about. When drawer #7 updates, only the people watching drawer #7 notice. Everyone else keeps working.

The Core Difference

Context has no selector mechanism. When the context value changes, EVERY component consuming that context re-renders — even if they only use one field from a large object.

const AppContext = createContext({
  user: null,
  theme: 'light',
  notifications: [],
  cart: [],
});
 
function CartIcon() {
  // This component only cares about cart.length
  const { cart } = useContext(AppContext);
  return <span>Cart ({cart.length})</span>;
}
 
// BUT: CartIcon re-renders when theme changes, when notifications update,
// when user logs in — every context change triggers a re-render here.

With Redux or Zustand, you select only what you need:

// Zustand — only re-renders when cart.length changes
const cartCount = useStore(state => state.cart.length);
 
// Redux Toolkit — same idea
const cartCount = useSelector(state => state.cart.length);

The Decision Framework

FactorContextRedux/Zustand
Update frequencyLow (theme, auth, locale)High (forms, real-time, lists)
Number of consumersFewMany
Need selectors?No selector supportYes, granular subscriptions
DevToolsBasicFull time-travel debugging
Bundle size impactZero (built-in)Additional dependency
Learning curveMinimalModerate

The Hybrid Approach (What Production Apps Actually Do)

Context          → Theme, auth, locale, feature flags (changes rarely)
Zustand/Redux    → UI state, shopping cart, form state (changes often)
React Query/SWR  → Server data (API responses, caching, revalidation)

Most real apps use all three. Context for dependency injection, an external store for client state, and a server-state library for API data.

The One-Liner That Impresses

"Context is a dependency injection mechanism, not a state manager — it's perfect for low-frequency, app-wide values like theme and auth. For high-frequency updates with many consumers, external stores like Zustand or Redux provide selector-based subscriptions that prevent the 'all consumers re-render' problem that Context can't avoid."

Common Follow-Up Questions

"Can you use multiple contexts to avoid the re-render problem?"

Yes — splitting one big context into multiple smaller ones helps. If theme and auth are separate contexts, a theme change won't re-render components that only consume auth. But with many contexts, you end up with "provider hell" — deeply nested providers at the root.

"What about the use hook in React 19 for context?"

The use hook lets you consume context (and promises) conditionally, but it still doesn't add selectors. The re-render-all-consumers behavior remains the same.

Red Flags in Your Answer

  • Saying "Context replaces Redux" without qualification
  • Not understanding why Context causes unnecessary re-renders
  • Recommending Redux for everything regardless of scale
  • Not mentioning server-state libraries (React Query, SWR) as part of the picture

Q20: What Are the Drawbacks of Context?

Interview Question: "What problems does React Context have?"

The Simple Explanation

Think of Context like a radio station. When the station broadcasts, EVERY radio tuned to that station hears it — you can't send a message to just one listener. If the station broadcasts every second, every radio is processing updates every second, even if they only care about the hourly news.

The Re-Render Problem (The Big One)

const UserContext = createContext({ name: '', preferences: { theme: 'light' } });
 
function UserProvider({ children }) {
  const [user, setUser] = useState({ name: 'Alice', preferences: { theme: 'light' } });
 
  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}
 
function ThemeDisplay() {
  const { preferences } = useContext(UserContext);
  return <p>Theme: {preferences.theme}</p>;
}
 
function NameDisplay() {
  const { name } = useContext(UserContext);
  return <p>Name: {name}</p>;
}
 
// When name changes → ThemeDisplay re-renders (even though it only reads theme)
// When theme changes → NameDisplay re-renders (even though it only reads name)

There's no built-in way to subscribe to just part of the context value.

Mitigation Strategies

1. Split contexts by concern

const ThemeContext = createContext('light');
const UserContext = createContext<User | null>(null);
const NotificationContext = createContext<Notification[]>([]);
 
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <NotificationProvider>
          <MainApp />
        </NotificationProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

Now a theme change doesn't affect components that only consume UserContext.

2. Memoize the provider value

function UserProvider({ children }) {
  const [user, setUser] = useState<User | null>(null);
 
  // Without useMemo, a new object is created every render,
  // causing all consumers to re-render every time UserProvider re-renders
  const value = useMemo(() => ({ user, setUser }), [user]);
 
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

3. Separate read and write contexts

const UserStateContext = createContext<User | null>(null);
const UserDispatchContext = createContext<(user: User) => void>(() => {});
 
function UserProvider({ children }) {
  const [user, setUser] = useState<User | null>(null);
 
  return (
    <UserStateContext.Provider value={user}>
      <UserDispatchContext.Provider value={setUser}>
        {children}
      </UserDispatchContext.Provider>
    </UserStateContext.Provider>
  );
}
 
// Components that only WRITE don't re-render when the value changes
function LogoutButton() {
  const setUser = useContext(UserDispatchContext);
  return <button onClick={() => setUser(null)}>Logout</button>;
}

Other Drawbacks Beyond Re-Renders

  • Provider hell — many contexts = deeply nested providers at the root
  • No DevTools — you can't time-travel debug or inspect context values easily (unlike Redux DevTools)
  • Testing overhead — every test needs the right providers wrapped around the component
  • No middleware — no built-in way to log, persist, or intercept state changes

The One-Liner That Impresses

"Context's main drawback is that it has no selector mechanism — when the provider value changes, every consumer re-renders regardless of which part of the value they use. You can mitigate this by splitting contexts, memoizing values, and separating read from write — but at some point, if you're fighting Context this hard, an external store with subscription-based selectors is the right tool."

Common Follow-Up Questions

"Does React.memo fix the Context re-render problem?"

No. React.memo prevents re-renders from parent props, but context is a separate trigger. If a component consumes a context and the context value changes, it will re-render even if it's wrapped in React.memo.

"Is the React team working on Context selectors?"

There have been proposals for useContextSelector but nothing has shipped in core React. Libraries like use-context-selector exist as workarounds, but the official recommendation is to use external stores for high-frequency updates.

Red Flags in Your Answer

  • Not knowing about the re-render-all-consumers problem
  • Not being able to suggest mitigation strategies
  • Saying "Context is fine for everything" without acknowledging limitations
  • Not knowing that React.memo doesn't help with context-triggered re-renders
  • Recommending Context for high-frequency state like form inputs or real-time data