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. :::