React & Modern Frontend Frameworks

Hooks Deep Dive & State Management

5 min read

React hooks are the backbone of modern React development. Interviewers test not just your ability to use hooks, but your understanding of their rules, pitfalls, and when to choose alternatives.

useEffect Cleanup Pitfalls

The most common source of bugs in React interviews:

// WRONG — memory leak, no cleanup
useEffect(() => {
  const interval = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // Missing cleanup! The interval runs forever
}, []);

// CORRECT — cleanup on unmount
useEffect(() => {
  const interval = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(interval);
}, []);
// WRONG — stale closure over state
useEffect(() => {
  const handler = () => {
    console.log(count); // Always logs the initial value
  };
  window.addEventListener('scroll', handler);
  return () => window.removeEventListener('scroll', handler);
}, []); // Empty deps = stale closure

// CORRECT — use ref for latest value
const countRef = useRef(count);
countRef.current = count;

useEffect(() => {
  const handler = () => {
    console.log(countRef.current); // Always current
  };
  window.addEventListener('scroll', handler);
  return () => window.removeEventListener('scroll', handler);
}, []);

useRef vs. useState

Know when to use each:

Feature useState useRef
Triggers re-render Yes No
Persists across renders Yes Yes
Available in render Current value .current
Use for UI state, displayed values Timers, DOM refs, previous values, mutable flags
// Classic interview question: implement usePrevious
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);

  return (
    <p>Now: {count}, Before: {prevCount}</p>
  );
}

Custom Hooks

Designing custom hooks demonstrates senior-level React skills:

// useDebounce: debounce a rapidly changing value
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

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

  return debouncedValue;
}

// useLocalStorage: persist state to localStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

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

  return [value, setValue];
}

// Usage
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);
  const [history, setHistory] = useLocalStorage('search-history', []);

  useEffect(() => {
    if (debouncedQuery) {
      fetchResults(debouncedQuery);
      setHistory(prev => [...prev, debouncedQuery].slice(-10));
    }
  }, [debouncedQuery]);

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

State Management: When to Use What

This is a common interview discussion topic:

React Context

// Good for: theme, locale, auth state (infrequent updates)
const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Page />
    </ThemeContext.Provider>
  );
}

// BAD for: frequently updating state (causes all consumers to re-render)

Zustand (lightweight external store)

// Good for: shared state that updates frequently, simple API
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 }),
}));

function Counter() {
  // Only re-renders when count changes, not other store values
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

TanStack Query (server state)

// Good for: API data fetching, caching, synchronization
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error message={error.message} />;
  return <div>{data.name}</div>;
}

Decision Matrix

Need Solution
Theme, locale, auth React Context
Form state useState / useReducer (local)
Complex local state useReducer
Shared client state Zustand or Jotai
Server data TanStack Query
Large-scale global state Redux Toolkit (if already in stack)

Interview tip: When asked about state management, demonstrate that you choose the right tool for the specific need rather than defaulting to one solution for everything. This shows architectural thinking.

Next, we'll explore Server Components and the Next.js architecture that's reshaping frontend development. :::

Quiz

Module 3: React & Modern Frontend Frameworks

Take Quiz