DEV Community

Cover image for I Regret Not Knowing These 7 React Secrets Before My Senior Interview (They Cost Me the Job) | Muhammad Arslan
Muhammad Arslan
Muhammad Arslan

Posted on Edited on Originally published at muhammadarslan.codes

I Regret Not Knowing These 7 React Secrets Before My Senior Interview (They Cost Me the Job) | Muhammad Arslan

I bombed three senior React interviews in a row. The worst part? Each interviewer asked variations of the same questions—and I got them all wrong. These weren't obscure trivia questions. They were fundamental patterns that separate React seniors from juniors.


By Muhammad Arslan

Senior Full Stack Engineer, Node.js Specialist, & React Performance Expert


Three months later, after building a real-time trading dashboard processing 50,000 updates per second, I finally understood what I was missing. The patterns that make React apps scream. The mental models that let you architect systems instead of just writing components.

Today I lead a team of 12 engineers. But I still remember that sinking feeling when an interviewer asked why my useEffect was running twice in development—and I had no idea.

Here are the 7 React secrets I wish someone had told me. Master these, and you'll walk into any interview—or code review—like you own the room.


Table of Contents

  1. Secret #1: useEffect Is a Code Smell (Most of the Time)
  2. Secret #2: The Double Render in React 18 Is Your Friend, Not Enemy
  3. Secret #3: You're Probably Deriving State Wrong
  4. Secret #4: The Hidden Performance Killer in Your Lists
  5. Secret #5: Context API Is a Footgun (Use This Instead)
  6. Secret #6: The Ref Pattern That Eliminates Stale Closures
  7. Secret #7: Server Components Change Everything
  8. Bonus: The One Question That Stumps 90% of Candidates

1. Secret #1: useEffect Is a Code Smell (Most of the Time)

Interview question that broke me: "Why do you need useEffect here?"

My answer: "To fetch data when the component mounts."

The interviewer's face said everything. Here's what I learned.

The Anti-Pattern Everyone Uses

// ❌ What I wrote (and what most tutorials teach)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetchUser(userId).then(user => {
      setUser(user);
      setLoading(false);
    });
  }, [userId]);

  if (loading) return <Spinner />;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The problems:

  • Race conditions if userId changes quickly
  • No caching between renders
  • Loading states handled manually
  • Error handling is an afterthought

What Seniors Do Instead

// ✅ The senior approach (React Query / SWR)
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The senior mindset: useEffect for data fetching is a code smell. It's 2026. Use a proper data library that handles caching, deduplication, and background updates automatically.

When You Actually Need useEffect

// ✅ Legitimate useEffect: integrating with non-React systems
function useKeyboardShortcut(key, callback) {
  useEffect(() => {
    const handler = (e) => {
      if (e.key === key) callback();
    };

    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [key, callback]);
}
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: If you're not subscribing to an external system (DOM events, WebSocket, animation frame), you probably don't need useEffect.


2. Secret #2: The Double Render in React 18 Is Your Friend, Not Enemy

Interviewer: "Why does your component render twice in development?"

Me: "That's a bug. I need to fix it."

Wrong. Dead wrong.

What's Actually Happening

React 18 intentionally double-invokes certain functions in development to help you find side effects:

function BadComponent() {
  const [count, setCount] = useState(0);

  // ❌ This runs twice in development!
  const data = fetchData(); // Side effect during render

  return <div>{data}</div>;
}
Enter fullscreen mode Exit fullscreen mode

If fetchData() has side effects (API call, DOM manipulation), you'll see it immediately because it runs twice.

The Real Purpose

React is testing whether your components are pure. A pure component:

  • Returns the same output for the same props/state
  • Has no side effects during render
  • Doesn't mutate external state
function GoodComponent() {
  const [count, setCount] = useState(0);

  // ✅ Safe: calculation based only on props/state
  const doubled = count * 2; // Pure calculation

  // ✅ Data fetching in the right place
  const { data } = useQuery({
    queryKey: ['data'],
    queryFn: fetchData, // Only runs once, properly cached
  });

  return <div>{doubled} - {data?.value}</div>;
}
Enter fullscreen mode Exit fullscreen mode

What Seniors Know

That double render caught a bug that would have corrupted production data. It's not a bug—it's a feature that saves your career.


3. Secret #3: You're Probably Deriving State Wrong

I spent months fighting bugs like "my filter doesn't update when data changes"—all because I didn't understand this pattern.

The Trap Everyone Falls Into

// ❌ The trap: redundant state
function ProductList({ products }) {
  const [filteredProducts, setFilteredProducts] = useState(products);
  const [searchTerm, setSearchTerm] = useState('');

  // Bug: filteredProducts doesn't update when products prop changes!
  useEffect(() => {
    setFilteredProducts(
      products.filter(p => 
        p.name.toLowerCase().includes(searchTerm.toLowerCase())
      )
    );
  }, [searchTerm, products]);

  return (
    <>
      <input 
        value={searchTerm} 
        onChange={e => setSearchTerm(e.target.value)} 
      />
      {filteredProducts.map(product => ...)}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Senior Pattern: Compute During Render

// ✅ The senior way: derived state during render
function ProductList({ products }) {
  const [searchTerm, setSearchTerm] = useState('');

  // Compute during render - always fresh, always correct
  const filteredProducts = useMemo(() => {
    console.log('Filtering', products.length, 'products');
    return products.filter(p => 
      p.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [products, searchTerm]);

  return (
    <>
      <input 
        value={searchTerm} 
        onChange={e => setSearchTerm(e.target.value)} 
      />
      {filteredProducts.map(product => ...)}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why this matters: No synchronization bugs. The filtered list is always derived from the latest props and state.

The Golden Rule

If you can calculate it from props or state, it shouldn't be in useState.


4. Secret #4: The Hidden Performance Killer in Your Lists

My trading dashboard was lagging at 500 rows. After this fix, it handled 50,000 rows at 60fps.

The Invisible Problem

// ❌ Looks innocent, kills performance
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} /> // 🚨 Using index as key
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Using index as key seems harmless until:

  • You sort the list → React re-renders everything instead of re-ordering
  • You add items at the start → Every single item re-renders
  • You have 10,000 items → Your app freezes

The Fix That Changed Everything

// ✅ Stable, unique keys for React reconciliation
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} /> // ✅ Unique, stable ID
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Advanced Pattern: Virtualization

// ✅ For massive lists: virtualization
import { VirtualList } from 'react-virtual';

function MassiveList({ items }) {
  return (
    <VirtualList
      height={500}
      itemCount={items.length}
      itemSize={50}
      renderItem={({ index, style }) => (
        <div key={items[index].id} style={style}>
          <Item data={items[index]} />
        </div>
      )}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

With virtualization, only visible items render. 50,000 items? Still 60fps.


5. Secret #5: Context API Is a Footgun (Use This Instead)

I once rebuilt our entire auth system because Context was causing 200ms re-render delays on every page. Here's what I learned.

The Context Performance Trap

// ❌ The trap: putting everything in one context
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('dark');
  const [notifications, setNotifications] = useState([]);
  // ... 20 more state values

  const value = { user, setUser, theme, setTheme, notifications, /* ... */ };

  return (
    <AppContext.Provider value={value}>
      {children} // 🚨 Everything re-renders when ANY value changes!
    </AppContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Senior Solution: Split Contexts + Zustand

// ✅ Split by concern - user data rarely changes
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

// Or better: use Zustand for state management
import { create } from 'zustand';

const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
}));

const useThemeStore = create((set) => ({
  theme: 'dark',
  setTheme: (theme) => set({ theme }),
}));

// Components subscribe only to what they need
function UserAvatar() {
  const user = useUserStore(state => state.user); // Only re-renders when user changes
  return <img src={user?.avatar} />;
}

function ThemeToggle() {
  const { theme, setTheme } = useThemeStore(); // Only re-renders when theme changes
  return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>🌓</button>;
}
Enter fullscreen mode Exit fullscreen mode

When Context Is Actually Good

// ✅ Context shines for dependency injection, not state
const ApiClientContext = createContext();

function App() {
  const apiClient = useMemo(() => new ApiClient({ baseURL: '/api' }), []);

  return (
    <ApiClientContext.Provider value={apiClient}>
      <Router />
    </ApiClientContext.Provider>
  );
}

// Deep nested component gets API client without prop drilling
function useApiClient() {
  return useContext(ApiClientContext);
}
Enter fullscreen mode Exit fullscreen mode

6. Secret #6: The Ref Pattern That Eliminates Stale Closures

This was the final question in my last failed interview. I got it wrong. Now I use this pattern daily.

The Stale Closure Problem

// ❌ Stale closure: always logs initial count
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(count); // Always 0! Stale closure
    }, 1000);

    return () => clearInterval(interval);
  }, []); // Empty deps - closure captures count=0 forever

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

The Ref Solution

// ✅ useRef keeps mutable reference without re-rendering
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // Keep ref in sync with state
  countRef.current = count;

  useEffect(() => {
    const interval = setInterval(() => {
      console.log(countRef.current); // Always fresh! ✅
    }, 1000);

    return () => clearInterval(interval);
  }, []); // No deps needed - ref is mutable

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

The Advanced Pattern: useLatest Hook

// ✅ Reusable pattern for fresh values in effects
function useLatest(value) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}

function ChatComponent({ userId }) {
  const [messages, setMessages] = useState([]);
  const latestMessages = useLatest(messages);

  useEffect(() => {
    const ws = new WebSocket(`ws://api.com/chat/${userId}`);

    ws.onmessage = (event) => {
      const newMessage = JSON.parse(event.data);
      // Always has fresh messages array
      setMessages([...latestMessages.current, newMessage]);
    };

    return () => ws.close();
  }, [userId]); // Only reconnect when userId changes

  return <MessageList messages={messages} />;
}
Enter fullscreen mode Exit fullscreen mode

7. Secret #7: Server Components Change Everything

This was the "extra credit" question that got me the job offer. Understanding this separates React developers from React architects.

The Mental Model Shift

In Next.js App Router (or any RSC framework), components default to Server Components:

// ✅ Server Component (default in App Router)
async function ProductPage() {
  // This runs on the server! Zero client JS
  const products = await db.query('SELECT * FROM products');

  return (
    <div>
      <h1>Products</h1>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens:

  • Database query runs on the server
  • HTML streams to the client
  • Zero JavaScript bundle for this component
  • SEO-friendly, instant first paint

When You Need Client Components

'use client'; // Mark as client component

// ✅ Only when you need browser APIs
function AddToCartButton({ productId }) {
  const [isAdding, setIsAdding] = useState(false);

  const addToCart = async () => {
    setIsAdding(true);
    await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ productId }) });
    setIsAdding(false);
  };

  return (
    <button onClick={addToCart} disabled={isAdding}>
      {isAdding ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Architecture Pattern

// ✅ Server Component fetches data
// ✅ Client Component handles interactivity
async function ProductPage() {
  const products = await getProducts();

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product}>
          <AddToCartButton productId={product.id} />
        </ProductCard>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The interview insight: Server Components aren't just about performance. They're about the right code running in the right place.


8. Bonus: The One Question That Stumps 90% of Candidates

The interviewer leaned back and asked: "When does React re-render a component?"

I gave the textbook answer: "When state or props change."

"Wrong," he said. "That's when React checks if it should re-render. The actual answer determines whether you understand React at a fundamental level."

The Real Answer

React re-renders a component when:

  1. The component's own state changes (via setState)
  2. The parent re-renders (causing this component to re-render regardless of props)
  3. Context value changes (if the component subscribes to it)

Props changing doesn't cause re-renders directly. The parent re-rendering causes both the re-render AND the new props.

The Fix: Memoization Strategy

// ✅ Control when child re-renders
const MemoizedChild = memo(function Child({ data }) {
  return <div>{data.name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const data = useMemo(() => ({ name: 'Static' }), []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      {/* Won't re-render when parent does - props are memoized */}
      <MemoizedChild data={data} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

These seven patterns took me from failing interviews to leading engineering teams. But here's the real secret: understanding why these patterns exist is more valuable than memorizing them.

React is becoming simpler. Server Components reduce client-side complexity. Compiler (React Forget) will eventually eliminate manual memoization. But these mental models—the separation of concerns, the purity of components, the right abstraction at the right layer—will serve you in any framework.

The engineer who understands the "why" will always outlast the engineer who only knows the "how."


Ready to master modern React architecture? I help teams build performant, scalable React applications. Let's discuss your project →


What pattern surprised you the most? Drop a comment—I read every single one.

Top comments (0)