Logo

Frontend Cheatsheet by Vlasis

React, TypeScript & Modern Web Development

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 mount
With Dependencies
useEffect(() => {
  // Runs when userId changes
  fetchUser(userId);
}, [userId]); // Re-run when userId changes
Cleanup 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
┌─────────────┬────────────────────────┬─────────────────────┬────────────────────────────────────┐
HookWhat 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 functionRecreated 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 semantic

Virtual 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 degradation

Never 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

Redux Toolkit

YouTube
  • 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;
      });
  },
});

Zustand

YouTube
  • 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 everything
Portal 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-friendly

Conditional Rendering & Event Handling

Conditional Rendering

YouTube
  • 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>}

Event Handling

YouTube
  • 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

React.memo

YouTube
  • 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} />
    </>
  );
}

Code Splitting & Lazy Loading

YouTube
  • 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.