React Essentials
Component patterns, hooks, and common pitfalls
Component Patterns
Controlled vs Uncontrolled Inputs
- •Controlled: React state is the single source of truth. Input value is controlled by state via value prop, and changes are handled through onChange.
- •Uncontrolled: Input value is managed by the DOM itself. You access the value using refs when needed.
- •Controlled advantages: Predictable, enables validation, easier to implement features like reset/clear, works well with form libraries.
- •Uncontrolled advantages: Less code, better performance for simple forms, easier integration with non-React code.
- •Rule of thumb: Use controlled for most cases, especially when you need validation or dynamic behavior.
Controlled Input
const [value, setValue] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
// Validation happens immediately
if (newValue.length < 3) {
setError('Must be at least 3 characters');
} else {
setError('');
}
};
<input
value={value}
onChange={handleChange}
className={error ? 'border-red-500' : ''}
/>
{error && <span className="text-red-500">{error}</span>}Uncontrolled Input
const inputRef = useRef(null);
const handleSubmit = () => {
// Only read value when needed (e.g., on submit)
console.log(inputRef.current.value);
};
<input ref={inputRef} defaultValue="initial" />
<button onClick={handleSubmit}>Submit</button>Lifting State Up
- •When multiple components need the same state, move it to their closest common ancestor.
- •This creates a single source of truth and prevents state synchronization issues.
- •Data flows down via props, events flow up via callback functions.
- •Alternative: Use Context API or state management library for deeply nested or widely shared state.
Before (State in Child)
// Problem: Both children need the same temperature
function TemperatureInput() {
const [temp, setTemp] = useState(0);
return <input value={temp} onChange={e => setTemp(e.target.value)} />;
}
function Display() {
const [temp, setTemp] = useState(0); // Duplicated!
return <div>{temp}°C</div>;
}After (State Lifted Up)
function App() {
// Single source of truth
const [temp, setTemp] = useState(0);
return (
<>
<TemperatureInput
value={temp}
onChange={setTemp}
/>
<Display value={temp} />
</>
);
}
function TemperatureInput({ value, onChange }) {
return <input
value={value}
onChange={e => onChange(e.target.value)}
/>;
}
function Display({ value }) {
return <div>{value}°C</div>;
}Passing Data from Child to Parent
- •Pass callback functions from parent to child. Child calls the callback with data.
- •Parent receives the data through the callback and can update its state.
- •Common pattern: Child triggers action, parent handles the result.
- •This is how forms, modals, and interactive components communicate with parents.
Basic Pattern
function Parent() {
const [data, setData] = useState(null);
// Callback function passed to child
const handleChildData = (childData) => {
setData(childData); // Parent receives data from child
};
return (
<div>
<p>Parent received: {data}</p>
<Child onSendData={handleChildData} />
</div>
);
}
function Child({ onSendData }) {
const handleClick = () => {
// Child calls parent's callback with data
onSendData('Hello from child!');
};
return <button onClick={handleClick}>Send Data to Parent</button>;
}Form Example
function FormContainer() {
const [formData, setFormData] = useState({});
const handleSubmit = (data) => {
setFormData(data);
// Process form data
};
return <Form onSubmit={handleSubmit} />;
}
function Form({ onSubmit }) {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
// Pass data to parent
onSubmit({ name });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}Modal Example
function App() {
const [isOpen, setIsOpen] = useState(false);
const [result, setResult] = useState(null);
const handleClose = (data) => {
setIsOpen(false);
if (data) setResult(data); // Get data from modal
};
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && <Modal onClose={handleClose} />}
{result && <p>Result: {result}</p>}
</>
);
}
function Modal({ onClose }) {
const handleConfirm = () => {
onClose({ confirmed: true }); // Send data to parent
};
return (
<div className="modal">
<button onClick={() => onClose(null)}>Cancel</button>
<button onClick={handleConfirm}>Confirm</button>
</div>
);
}Container vs Presentational Components
- •Container (Smart) Components: Handle logic, state management, data fetching, and business rules. They know HOW things work.
- •Presentational (Dumb) Components: Only handle UI rendering. They receive data and callbacks via props. They know WHAT to display.
- •Benefits: Easier testing (test logic separately from UI), better reusability (same UI with different data sources), clearer separation of concerns.
- •Modern approach: Hooks allow mixing logic and UI, but the principle of separating concerns remains valuable.
Container Component
function UserContainer() {
// Logic and state
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// Pass data to presentational component
return (
<UserView
user={user}
loading={loading}
error={error}
/>
);
}Presentational Component
// Pure UI component - no logic, just rendering
function UserView({ user, loading, error }) {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <EmptyState />;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}Hooks
useState
- •Returns [stateValue, setStateFunction]. State updates are asynchronous and batched for performance.
- •Multiple setState calls in the same event handler are batched together - React waits until the handler finishes before re-rendering.
- •Use functional updates (setState(prev => prev + 1)) when the new state depends on the previous state.
- •State updates trigger re-renders. Only the component and its children re-render, not siblings or parents.
- •Initial state can be a value or a function (lazy initialization) for expensive computations.
Batching Example
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // count is still 0
setCount(count + 1); // count is still 0
// Result: count becomes 1 (not 2!)
};
// Fix: Use functional update
const handleClickFixed = () => {
setCount(prev => prev + 1); // prev = 0, returns 1
setCount(prev => prev + 1); // prev = 1, returns 2
// Result: count becomes 2 ✓
};
}Lazy Initialization
// Expensive computation runs on every render
const [data, setData] = useState(expensiveFunction());
// Fix: Only runs once on mount
const [data, setData] = useState(() => expensiveFunction());useEffect
- •Runs side effects after render: API calls, subscriptions, DOM manipulation, timers.
- •Empty deps []: Runs once after initial mount (componentDidMount equivalent).
- •With deps [a, b]: Runs after mount AND whenever a or b changes (componentDidUpdate equivalent).
- •No deps: Runs after EVERY render (usually a bug - causes infinite loops).
- •Cleanup function: Return a function to clean up (unsubscribe, clear timers) - runs before next effect or on unmount.
- •Common mistake: Missing dependencies causes stale closures and bugs.
Mount Only (Empty Deps)
useEffect(() => {
// Runs once after component mounts
fetchUserData();
}, []); // Empty array = only on mountWith Dependencies
useEffect(() => {
// Runs when userId changes
fetchUser(userId);
}, [userId]); // Re-run when userId changesCleanup Function
useEffect(() => {
const subscription = subscribe();
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// Cleanup: runs before next effect or on unmount
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, []);Common Bug - Missing Deps
// ❌ BUG: Missing userId in deps
useEffect(() => {
fetchUser(userId);
}, []); // userId might be stale!
// ✅ FIX: Include all dependencies
useEffect(() => {
fetchUser(userId);
}, [userId]);useRef
- •Creates a mutable reference that persists across renders without causing re-renders.
- •Changing .current does NOT trigger a re-render (unlike useState).
- •Common uses: DOM element access, storing previous values, keeping mutable values that don't need to trigger renders, avoiding stale closures.
- •Refs are like instance variables in class components - they persist but don't cause updates.
DOM Access
function InputFocus() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // Direct DOM manipulation
};
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>Focus Input</button>
</>
);
}Storing Previous Value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // Store after render
});
return ref.current; // Return previous value
}
function Component({ count }) {
const prevCount = usePrevious(count);
return <div>Previous: {prevCount}, Current: {count}</div>;
}Avoiding Stale Closures
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1); // Functional update avoids stale closure
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
}useMemo
- •Memoizes the result of an expensive computation. Only recalculates when dependencies change.
- •Use when: Calculation is expensive (loops, complex math), or you need referential equality for object/array props.
- •Don't overuse: Memoization itself has overhead. Only use if you measure a performance problem.
- •Common mistake: Memoizing everything - React is already fast, only optimize when needed.
Expensive Calculation
function ExpensiveComponent({ items }) {
// ❌ Recalculates on every render
const sorted = items.sort((a, b) => a.value - b.value);
// ✅ Only recalculates when items change
const sorted = useMemo(
() => items.sort((a, b) => a.value - b.value),
[items]
);
return <div>{/* render sorted items */}</div>;
}Referential Equality
function Parent() {
const [count, setCount] = useState(0);
// ❌ New object every render - Child re-renders
const config = { theme: 'dark', size: 'large' };
// ✅ Same reference if deps unchanged - Child doesn't re-render
const config = useMemo(
() => ({ theme: 'dark', size: 'large' }),
[]
);
return <Child config={config} />;
}useCallback
- •Memoizes a function so it keeps the same reference between renders unless dependencies change.
- •Use when: Passing functions to memoized children (React.memo), or functions in dependency arrays.
- •Prevents unnecessary re-renders of child components that receive the function as a prop.
- •Same rule as useMemo: Don't overuse - only when you have a measured performance issue.
Preventing Child Re-renders
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ New function every render - Child re-renders
const handleClick = () => console.log('clicked');
// ✅ Same function reference - Child doesn't re-render
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</>
);
}With Dependencies
function Search({ query }) {
// Function depends on query, so include it in deps
const handleSearch = useCallback((term) => {
searchAPI(term, query);
}, [query]); // Recreate when query changes
return <SearchInput onSearch={handleSearch} />;
}useRef vs useMemo vs useCallback - Quick Comparison
- •Understanding when to use each hook is crucial for React interviews.
- •All three help with performance and avoiding unnecessary work, but solve different problems.
Comparison Table
┌─────────────┬────────────────────────┬─────────────────────┬────────────────────────────────────┐
│ Hook │ What it stores │ When it updates │ Why you use it │
├─────────────┼────────────────────────┼─────────────────────┼────────────────────────────────────┤
│ useRef │ A mutable value │ Never causes │ Keep a value between renders │
│ │ (.current) OR a │ re-render │ without re-rendering; access DOM │
│ │ DOM element │ │ │
├─────────────┼────────────────────────┼─────────────────────┼────────────────────────────────────┤
│ useMemo │ A computed value │ Recomputed only │ Avoid recalculating expensive │
│ │ (result) │ when deps change │ values │
├─────────────┼────────────────────────┼─────────────────────┼────────────────────────────────────┤
│ useCallback │ A memoized function │ Recreated only │ Avoid re-creating functions │
│ │ │ when deps change │ (useful for performance + │
│ │ │ │ stable deps) │
└─────────────┴────────────────────────┴─────────────────────┴────────────────────────────────────┘Key Differences
// useRef: Mutable, doesn't trigger re-render
const countRef = useRef(0);
countRef.current = 5; // ✅ No re-render
// useMemo: Computes and caches a value
const sum = useMemo(() => a + b, [a, b]); // ✅ Recomputes when a or b changes
// useCallback: Memoizes a function reference
const fn = useCallback(() => {}, [deps]); // ✅ Same function reference if deps unchanged
// Common mistake: Using useMemo for functions
const fn = useMemo(() => () => {}, []); // ❌ Works but useCallback is clearer
const fn = useCallback(() => {}, []); // ✅ Better - more semanticVirtual DOM & Keys
Virtual DOM (vDOM)
- •In-memory copy of the real DOMReact. Creates a lightweight JavaScript object that represents what the real DOM should look like. This copy is the Virtual DOM
- •Re-rendering happens in memory. When a component’s state or props change, React does not touch the real DOM immediately. Instead, it re-renders a new vDOM tree in memory. This is fast because it's just JavaScript objects.
- •React compares the new vDOM with the previous one. This process is called reconciliation. React checks: 'What changed since last time?'
- •Only the necessary changes are applied to the actual DOM Instead of updating everything, React updates only the specific nodes that changed. This keeps the UI fast and smooth.
- •Important note: vDOM isn’t magically faster than the real DOM The key performance win is that React avoids unnecessary DOM updates by batching and minimizing them.
React keeps a JavaScript object representation of your UI.
<div>
<h1>Hello</h1>
<button>Click</button>
</div>✅ React creates something like:
{
type: "div",
children: [
{ type: "h1", children: ["Hello"] },
{ type: "button", children: ["Click"] }
]
}Keys in Lists
- •Keys help React identify which items changed, were added, or removed during reconciliation.
- •Without stable keys, React may incorrectly reuse DOM nodes, causing bugs like wrong state, lost focus, or performance issues.
- •Keys must be unique among siblings (not globally unique).
- •Never use index as key when: Items can be reordered, added, or removed. Index changes when list changes, breaking React's tracking.
- •When to use index: Static lists that never change order or length (rare in real apps).
❌ Bad - Index as Key
// Problem: If items reorder, React reuses wrong components
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
// If todo[0] is deleted, todo[1] becomes key={0}
// React thinks it's the same component!
))}
</ul>
);
}✅ Good - Unique ID as Key
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} />
// Stable key - React correctly tracks each item
))}
</ul>
);
}What Happens Without Keys
// React can't tell which item is which
// Results in:
// - Wrong component state after reordering
// - Lost input focus
// - Unnecessary DOM recreation
// - Performance degradationNever use array index as key when list can change order or items can be added/removed
Context API & State Management
Context API
- •Provides a way to pass data through the component tree without prop drilling.
- •Use when: Data needs to be accessible by many components at different nesting levels.
- •Create context with createContext(), provide value with Provider, consume with useContext() hook.
- •Performance: Context value changes cause ALL consumers to re-render. Split contexts by update frequency.
- •Not a replacement for state management: Use for theme, auth, language - not for frequently changing data.
Creating Context
// 1. Create context
const ThemeContext = createContext('light');
// 2. Provide value
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. Consume in any child
function Button() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
className={theme === 'dark' ? 'dark' : 'light'}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme
</button>
);
}Splitting Contexts for Performance
// ❌ Bad: Everything in one context
const AppContext = createContext();
// Theme changes cause User re-render!
// ✅ Good: Split by update frequency
const ThemeContext = createContext();
const UserContext = createContext();
// Theme changes don't affect User consumers- •Official Redux toolset for efficient Redux development. Simplifies Redux setup and reduces boilerplate.
- •Key features: configureStore (simplified store setup), createSlice (reducers + actions), createAsyncThunk (async logic).
- •Use when: Complex global state, time-travel debugging needed, large teams, predictable state updates.
- •Best practices: Keep state normalized, use selectors for derived data, keep slices focused and small.
Basic Redux Toolkit Setup
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Immer allows direct mutation
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;Using Redux in Components
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
);
}Async Actions with createAsyncThunk
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// Async thunk
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (userId) => {
const response = await fetch('/api/users/' + userId);
return response.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});- •Lightweight state management library. Simpler than Redux, more powerful than Context.
- •No providers needed, hooks-based API, minimal boilerplate, great TypeScript support.
- •Use when: Need global state but Redux is overkill, want simplicity, prefer hooks API.
- •Best for: Medium complexity apps, when you need state management but not Redux's complexity.
Basic Zustand Store
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Use in component
function Counter() {
const { count, increment, decrement } = useStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}With TypeScript
import { create } from 'zustand';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
}
const useCounterStore = create<CounterState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));Selective Subscriptions
// Only re-render when specific part of state changes
function CountDisplay() {
// Only subscribes to count, not other state
const count = useStore((state) => state.count);
return <div>{count}</div>;
}
function Controls() {
// Only subscribes to actions
const increment = useStore((state) => state.increment);
const decrement = useStore((state) => state.decrement);
return (
<>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
}Props vs State
- •Props: Data passed FROM parent TO child. Props are read-only in the child. Changes to props come from parent re-rendering with new props.
- •State: Data managed WITHIN a component. State changes trigger re-renders. State is private to the component.
- •Rule: If data can be derived from props or other state, it shouldn't be state. Use props for configuration, state for interactivity.
- •Lifting state: When multiple components need the same data, lift it to their common parent.
Props (From Parent)
function Parent() {
const [name, setName] = useState('John');
return <Child name={name} />; // name is a prop
}
function Child({ name }) {
// name is read-only - can't change it here
// Parent controls the value
return <div>{name}</div>;
}State (Internal)
function Counter() {
const [count, setCount] = useState(0); // Internal state
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
}Derived State (Anti-pattern)
// ❌ Bad: Storing derived data in state
function UserProfile({ user }) {
const [fullName, setFullName] = useState(user.firstName + user.lastName);
// fullName can get out of sync with user prop!
// ✅ Good: Compute from props
const fullName = user.firstName + user.lastName;
}Advanced React Patterns
React Portals (Rendering Outside Component Tree)
- •Portals allow you to render children into a DOM node outside the parent component's DOM hierarchy.
- •Use cases: Modals, tooltips, dropdowns, notifications that need to escape parent overflow/z-index constraints.
- •Created with ReactDOM.createPortal(child, container). Child renders in container but events bubble to React tree.
- •Common pattern: Render modals at document.body level to avoid z-index and overflow issues.
Basic Portal Example
import { createPortal } from 'react-dom';
function Modal({ children, isOpen }) {
if (!isOpen) return null;
// Render modal content into document.body
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.body // Portal target
);
}
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="app">
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Modal isOpen={isOpen}>
<p>This renders outside the app div!</p>
</Modal>
</div>
);
}Why Use Portals?
// ❌ Problem: Modal trapped by parent overflow
<div style={{ overflow: 'hidden', height: '100px' }}>
<Modal> {/* Modal gets clipped! */} </Modal>
</div>
// ✅ Solution: Portal renders at body level
<div style={{ overflow: 'hidden', height: '100px' }}>
<Modal> {/* Portal escapes to body, not clipped */} </Modal>
</div>
// Also solves z-index issues - portal content can be above everythingPortal with Event Handling
function Modal({ children, onClose }) {
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
return createPortal(
<div
className="modal-overlay"
onClick={onClose} // Click outside to close
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
>
{children}
</div>
</div>,
document.body
);
}Server-Side Rendering (SSR) with React
- •SSR: Render React components on the server, send HTML to client, then hydrate on client.
- •Benefits: Faster initial load, better SEO, works without JavaScript, better for slow connections.
- •Next.js: Built-in SSR support. Use getServerSideProps for dynamic SSR, or static generation for better performance.
- •Basic SSR: Use ReactDOMServer.renderToString() on server, ReactDOM.hydrateRoot() on client.
Basic SSR Implementation
// server.js (Node.js + Express)
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
const app = express();
app.get('/', (req, res) => {
// Render React component to HTML string
const html = ReactDOMServer.renderToString(<App />);
// Send HTML to client
res.send(`
<!DOCTYPE html>
<html>
<head><title>My App</title></head>
<body>
<div id="root">${html}</div>
<script src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);Client Hydration
// client.js
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// Hydrate the server-rendered HTML
// Attaches event listeners and makes it interactive
hydrateRoot(
document.getElementById('root'),
<App />
);Next.js SSR (Simpler)
// pages/user/[id].js (Next.js Pages Router)
export async function getServerSideProps(context) {
const { id } = context.params;
// Fetch data on server
const user = await fetchUser(id);
return {
props: {
user, // Passed to component as props
},
};
}
export default function UserPage({ user }) {
// Component receives data, rendered on server
return <div>{user.name}</div>;
}
// App Router (Next.js 13+)
// app/user/[id]/page.tsx
async function UserPage({ params }) {
// Server Component - runs on server by default
const user = await fetchUser(params.id);
return <div>{user.name}</div>;
}SSR vs CSR Comparison
// Client-Side Rendering (CSR)
// 1. Browser requests page
// 2. Server sends empty HTML + JS bundle
// 3. Browser downloads JS
// 4. JS renders React app
// 5. App fetches data
// 6. UI updates
// Result: Slower initial load, blank screen initially
// Server-Side Rendering (SSR)
// 1. Browser requests page
// 2. Server renders React to HTML
// 3. Server fetches data
// 4. Server sends complete HTML
// 5. Browser shows content immediately
// 6. JS hydrates (makes interactive)
// Result: Faster perceived load, SEO-friendlyConditional Rendering & Event Handling
- •Use && for simple conditions, ternary for if/else, early returns for complex logic.
- •Be careful with && - if left side is 0 or false, it will render. Use !! or explicit comparison.
- •Pattern: Extract complex conditionals into variables or separate components for readability.
Common Patterns
function Component({ user, items }) {
// Simple condition
return (
<>
{user && <UserProfile user={user} />}
{items.length > 0 && <ItemList items={items} />}
</>
);
// If/else
return user ? <Dashboard /> : <Login />;
// Early return for complex logic
if (!user) return <Login />;
if (user.isLoading) return <Spinner />;
return <Dashboard user={user} />;
}⚠️ Common Bug with &&
// ❌ BUG: If count is 0, it renders "0"!
{count && <div>You have {count} items</div>}
// ✅ FIX: Explicit boolean conversion
{count > 0 && <div>You have {count} items</div>}
{!!count && <div>You have {count} items</div>}
{Boolean(count) && <div>You have {count} items</div>}- •React events are SyntheticEvents - cross-browser wrappers around native events.
- •Event handlers receive the SyntheticEvent object. Use e.preventDefault() and e.stopPropagation() as needed.
- •Binding: Arrow functions or useCallback for class methods. In functional components, just define the function.
- •Passing arguments: Use arrow functions or bind. Be careful not to create new functions on every render.
Basic Event Handling
function Button() {
const handleClick = (e) => {
e.preventDefault(); // Prevent default behavior
e.stopPropagation(); // Stop event bubbling
console.log('Clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}Passing Arguments
function ItemList({ items }) {
// ✅ Good: Arrow function
const handleDelete = (id) => {
deleteItem(id);
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => handleDelete(item.id)}>
Delete
</button>
</li>
))}
</ul>
);
}Form Handling
function Form() {
const [value, setValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page reload
console.log('Submitted:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}Performance & Optimization
- •Higher-order component that memoizes a component. Only re-renders if props change (shallow comparison).
- •Use for: Expensive components that receive the same props frequently, or components that re-render unnecessarily.
- •Don't overuse: Memoization has overhead. Only use when you measure a performance problem.
- •Works with useMemo and useCallback: Memoized props prevent unnecessary re-renders.
Basic Usage
// Component only re-renders if name or age changes
const UserCard = React.memo(function UserCard({ name, age }) {
return (
<div>
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
});
// Custom comparison function
const UserCard = React.memo(
UserCard,
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.name === nextProps.name;
}
);With useCallback
const ExpensiveChild = React.memo(({ onClick }) => {
// Only re-renders if onClick reference changes
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback: new function every render
// With useCallback: same function reference
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveChild onClick={handleClick} />
</>
);
}- •Split your code into smaller chunks loaded on demand. Reduces initial bundle size.
- •Use React.lazy() with Suspense to lazy-load components.
- •Route-based splitting: Load components when routes are accessed.
- •Benefits: Faster initial load, better user experience, reduced memory usage.
Lazy Loading Component
import { lazy, Suspense } from 'react';
// Lazy load the component
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}Route-Based Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Interview Tip
React interviews expect strong reasoning about data flow, rerender behavior, side-effects, and component patterns. Focus on explaining WHY React works this way, not just HOW.