React

Component-based UIs, hooks, state management, performance optimization, and the mental model behind React's render cycle.

reacthooksstatecomponentsjsxperformance

What is React?

React is a JavaScript library for building user interfaces, created by Facebook/Meta in 2013 and open-sourced. It's the most popular frontend library in the world.

Analogy: Traditional web development is like repainting an entire wall
every time one square inch changes. React is like having smart tiles —
only the changed tile repaints, automatically.

Core idea: Build UIs as a tree of components — small, reusable pieces of UI. Each component is a function that takes props (inputs) and returns JSX (HTML-like syntax that describes the UI). React figures out the minimum DOM updates needed when state changes.

JSX — JavaScript + XML

JSX is a syntax extension that lets you write HTML-like code inside JavaScript:

// JSX
const element = <h1 className="title">Hello, {user.name}!</h1>;

// What it compiles to (React.createElement calls)
const element = React.createElement("h1", { className: "title" }, `Hello, ${user.name}!`);

Key JSX rules:

// 1. Use className, not class (class is a JS keyword)
<div className="card">...</div>

// 2. Self-close tags without children
<input type="text" />
<br />

// 3. Return one root element (or use React Fragment to avoid extra DOM node)
return (
  <>
    <h1>Title</h1>
    <p>Content</p>
  </>
);

// 4. JavaScript expressions in {}
<p>2 + 2 = {2 + 2}</p>
<button onClick={() => alert("clicked")}>Click</button>

// 5. Conditional rendering
{isLoggedIn && <UserMenu />}
{isLoggedIn ? <UserMenu /> : <LoginButton />}

// 6. List rendering — always provide a key
{users.map(user => <li key={user.id}>{user.name}</li>)}

Components & Props

A component is a function that returns JSX:

// A simple component
interface CardProps {
  title: string;
  description: string;
  imageUrl?: string;
}

function Card({ title, description, imageUrl }: CardProps) {
  return (
    <div className="card">
      {imageUrl && <img src={imageUrl} alt={title} />}
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  );
}

// Using it
<Card
  title="Introduction to React"
  description="Learn React from scratch"
  imageUrl="/react-thumbnail.jpg"
/>

Props are read-only — a component never modifies its own props. If you need to change data, lift it to state.

useState — Local Component State

import { useState } from "react";

function Counter() {
  // useState returns [currentValue, setter]
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(prev => prev - 1)}>-</button>
      {/* Use functional update (prev =>) when new state depends on old state */}
    </div>
  );
}

// State with objects
function Form() {
  const [form, setForm] = useState({ name: "", email: "" });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
    // Spread is essential — setState replaces, not merges, for hooks
  };

  return (
    <form>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
    </form>
  );
}

React's rendering rule: Every setState call schedules a re-render. React batches them (since React 18, all updates are batched, even in async code).

useEffect — Side Effects

useEffect handles operations outside the render cycle: data fetching, subscriptions, timers, DOM manipulation.

import { useState, useEffect } from "react";

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;  // prevent state update after unmount

    setLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    // Cleanup function — runs on unmount or before next effect
    return () => { cancelled = true; };
  }, [userId]); // dependency array — re-runs when userId changes

  if (loading) return <p>Loading...</p>;
  if (error)   return <p>Error: {error}</p>;
  if (!user)   return null;
  return <div>{user.name}</div>;
}

Dependency array rules:

  • [] — runs once on mount, cleanup on unmount (like componentDidMount)
  • [dep1, dep2] — runs when any dependency changes
  • No array — runs after every render (usually a bug)

useRef

useRef stores a mutable value that persists across renders without causing re-renders:

import { useRef, useEffect } from "react";

// 1. Accessing DOM elements
function SearchInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();  // auto-focus on mount
  }, []);

  return <input ref={inputRef} type="search" />;
}

// 2. Storing previous values
function Component({ value }: { value: number }) {
  const prevValue = useRef(value);

  useEffect(() => {
    console.log(`Changed from ${prevValue.current} to ${value}`);
    prevValue.current = value;
  }, [value]);
}

// 3. Storing mutable values that shouldn't trigger re-renders
function Timer() {
  const intervalId = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    intervalId.current = setInterval(() => console.log("tick"), 1000);
  };
  const stop = () => {
    if (intervalId.current) clearInterval(intervalId.current);
  };
}

useContext — Prop Drilling Solution

Passing props through many layers is called prop drilling — it's painful. Context provides a way to share values across the tree without explicit prop passing.

// 1. Create context
interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}
const ThemeContext = React.createContext<ThemeContextType | null>(null);

// 2. Provide it
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const toggle = () => setTheme(t => t === "light" ? "dark" : "light");

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Custom hook for clean usage
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error("useTheme must be used within ThemeProvider");
  return context;
}

// 4. Consume anywhere in the tree
function Header() {
  const { theme, toggle } = useTheme();
  return <button onClick={toggle}>Current: {theme}</button>;
}

useReducer — Complex State Logic

When useState gets complex (many related state updates, next state depends on current), use useReducer:

type State = { count: number; step: number };
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "setStep"; payload: number }
  | { type: "reset" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment": return { ...state, count: state.count + state.step };
    case "decrement": return { ...state, count: state.count - state.step };
    case "setStep":   return { ...state, step: action.payload };
    case "reset":     return { count: 0, step: 1 };
    default:          return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: "setStep", payload: Number(e.target.value) })}
      />
    </div>
  );
}

Custom Hooks

Extract reusable stateful logic into custom hooks (functions that start with use):

// Reusable data fetching hook
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => { if (!cancelled) { setData(d); setLoading(false); }})
      .catch(e => { if (!cancelled) { setError(e); setLoading(false); }});
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}

// Usage — zero boilerplate at the call site
function UserList() {
  const { data: users, loading, error } = useFetch<User[]>("/api/users");
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Performance Optimization

React.memo — Skip Re-renders

// Without memo: re-renders every time parent re-renders
function ExpensiveList({ items }: { items: string[] }) {
  console.log("Rendering list...");
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
}

// With memo: only re-renders if items prop actually changed
const ExpensiveList = React.memo(({ items }: { items: string[] }) => {
  return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
});

useMemo & useCallback

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // useMemo: only recomputes if items changes (not when count/text changes)
  const sortedItems = useMemo(
    () => [...items].sort((a, b) => a.localeCompare(b)),
    [items]
  );

  // useCallback: stable function reference (important for memo'd children)
  const handleClick = useCallback((id: number) => {
    console.log("clicked", id);
  }, []); // no deps: same function reference every render

  return <ExpensiveList items={sortedItems} onItemClick={handleClick} />;
}

Don't over-optimize: Only add memo/useMemo/useCallback when you have a measured performance problem. They add complexity and have their own overhead.

Common Interview Questions

Practice

  1. Beginner: Build a controlled form with name, email, and password fields, a submit handler, and basic validation (all fields required, email format, password min 8 chars).
  2. Core: Implement a useLocalStorage<T>(key, initialValue) hook that syncs state to localStorage.
  3. Context: Build a shopping cart with CartContext — add/remove items, update quantity, calculate total. Cart count shows in the navbar.
  4. Performance: Given a list of 10,000 items with a search filter, optimize it so filtering is fast using useMemo and the list doesn't re-render unnecessarily with React.memo.

Next: Next.js — React for production with SSR, SSG, and full-stack capabilities.