React Development · Lesson 2 of 15

Core Fundamentals

What React Actually Is

React is a UI library — not a framework. It renders a tree of components and re-renders only what changed. That's the whole contract.

Everything else — routing, state management, data fetching — you choose separately. This is React's biggest strength and most common source of confusion for newcomers.

JSX: JavaScript + HTML, Explained Properly

JSX looks like HTML but compiles to JavaScript function calls.

JSX
// What you write
const button = <button className="btn" onClick={handleClick}>Save</button>;

// What Babel compiles it to
const button = React.createElement(
  "button",
  { className: "btn", onClick: handleClick },
  "Save"
);

Key JSX rules that trip people up:

JSX
// 1. className, not class
<div className="container">

// 2. Expressions in {}, not {{ }}
<span>{user.name}</span>
<span>{isAdmin ? "Admin" : "User"}</span>

// 3. Self-closing tags are mandatory
<img src={logo} alt="Logo" />
<br />

// 4. Single root element (or Fragment)
return (
  <>
    <Header />
    <Main />
  </>
);

// 5. camelCase event handlers
<input onChange={handleChange} onKeyDown={handleKey} />

Real-World JSX: Dashboard Card Component

JSX
function MetricCard({ title, value, trend, icon: Icon }) {
  const isPositive = trend > 0;

  return (
    <div className={`card ${isPositive ? "card--positive" : "card--negative"}`}>
      <div className="card__header">
        <Icon size={20} />
        <span className="card__title">{title}</span>
      </div>
      <div className="card__value">{value}</div>
      <div className="card__trend">
        {isPositive ? "" : ""} {Math.abs(trend)}%
      </div>
    </div>
  );
}

// Usage
<MetricCard
  title="Monthly Revenue"
  value="$42,800"
  trend={12.5}
  icon={DollarSign}
/>

The Virtual DOM: Why React Is Fast

React keeps a virtual copy of the DOM in memory. When state changes:

  1. React creates a new virtual DOM tree
  2. Diffs it against the previous tree (reconciliation)
  3. Computes the minimum set of real DOM changes
  4. Applies only those changes (commit phase)
State Change
     ↓
New Virtual DOM Tree
     ↓
Diff with Previous Virtual DOM  ← This is cheap (pure JS objects)
     ↓
Minimal Real DOM Updates        ← This is expensive (avoided when possible)

Why this matters in practice:

JSX
// Without React: browser updates the entire list on every keystroke
document.getElementById("list").innerHTML = items.map(renderItem).join("");

// With React: only changed items re-render
function ItemList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>  // key helps React identify changes
      ))}
    </ul>
  );
}

The key prop is how React matches elements between renders. Use stable, unique IDs — never array indices for dynamic lists.

Functional vs Class Components

Use functional components. Class components are legacy. Here's why:

JSX
// Class component (legacy, avoid for new code)
class UserGreeting extends React.Component {
  constructor(props) {
    super(props);
    this.state = { greeting: "Hello" };
  }

  componentDidMount() {
    // fetch data here
  }

  render() {
    return (
      <h1>
        {this.state.greeting}, {this.props.name}!
      </h1>
    );
  }
}

// Functional component (modern, preferred)
function UserGreeting({ name }) {
  const [greeting, setGreeting] = useState("Hello");

  useEffect(() => {
    // fetch data here
  }, []);

  return <h1>{greeting}, {name}!</h1>;
}

Functional components are:

  • Shorter and easier to read
  • Easier to test
  • Better for performance optimizations
  • The only way to use hooks

Props: The Component API

Props are how parent components talk to children. Treat them as read-only.

JSX
// Define clear prop types in TypeScript
interface UserCardProps {
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  avatar?: string;          // Optional
  onDelete: (id: string) => void;  // Callback prop
}

function UserCard({ name, email, role, avatar, onDelete }: UserCardProps) {
  return (
    <div className="user-card">
      <img
        src={avatar ?? "/default-avatar.png"}
        alt={`${name}'s avatar`}
      />
      <div className="user-card__info">
        <h3>{name}</h3>
        <p>{email}</p>
        <span className={`badge badge--${role}`}>{role}</span>
      </div>
      <button onClick={() => onDelete(email)}>Remove</button>
    </div>
  );
}

Prop Patterns You'll Use Daily

JSX
// 1. Children prop — composable components
function Card({ children, className = "" }) {
  return (
    <div className={`card ${className}`}>
      {children}
    </div>
  );
}

// 2. Spread props — forwarding attributes
function Input({ label, error, ...inputProps }) {
  return (
    <div>
      <label>{label}</label>
      <input {...inputProps} className={error ? "input--error" : ""} />
      {error && <span className="error-text">{error}</span>}
    </div>
  );
}

// 3. Render prop — inversion of control
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((r) => r.json())
      .then((data) => { setData(data); setLoading(false); });
  }, [url]);

  return render({ data, loading });
}

// Usage
<DataFetcher
  url="/api/users"
  render={({ data, loading }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
/>

State: Component Memory

State is data that, when changed, causes the component to re-render.

JSX
// Correct: updating state immutably
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Learn React", done: false },
    { id: 2, text: "Build something", done: false },
  ]);

  const toggleTodo = (id) => {
    // Never mutate state directly
    // BAD: todos[0].done = true;

    // GOOD: create new array with updated item
    setTodos((prev) =>
      prev.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  const addTodo = (text) => {
    setTodos((prev) => [
      ...prev,
      { id: Date.now(), text, done: false },
    ]);
  };

  const deleteTodo = (id) => {
    setTodos((prev) => prev.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <AddTodoForm onAdd={addTodo} />
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
        />
      ))}
    </div>
  );
}

State Batching (React 18+)

React 18 batches all state updates automatically, even in async code:

JSX
function handleSave() {
  // React 18: these three updates cause ONE re-render
  setName("Alice");
  setAge(30);
  setLoading(false);

  // In async functions — also batched in React 18
  setTimeout(() => {
    setCount((c) => c + 1);   // React 18: still batched
    setActive(true);           // ONE re-render
  }, 1000);
}

When to Use Local State vs Lifted State

JSX
// Local state: only this component needs it
function SearchInput() {
  const [query, setQuery] = useState("");
  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

// Lifted state: multiple siblings need the same data
function FilterableTable() {
  const [filter, setFilter] = useState("");  // ← lifted here

  return (
    <>
      <FilterInput value={filter} onChange={setFilter} />
      <DataTable data={data} filter={filter} />
    </>
  );
}

Component Lifecycle (in Hooks)

The mental model for functional component lifecycle:

Mount   →  Render  →  Commit to DOM  →  Run effects
Update  →  Render  →  Commit to DOM  →  Cleanup old effects → Run new effects
Unmount →  Cleanup effects
JSX
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // componentDidMount + componentDidUpdate equivalent
  useEffect(() => {
    let cancelled = false;

    async function loadUser() {
      const data = await fetchUser(userId);
      if (!cancelled) setUser(data);  // prevent setting state after unmount
    }

    loadUser();

    // componentWillUnmount equivalent
    return () => {
      cancelled = true;
    };
  }, [userId]);  // re-runs when userId changes

  if (!user) return <Skeleton />;
  return <Profile user={user} />;
}

Conditional Rendering Patterns

JSX
function Notification({ type, message, count }) {
  // 1. Early return (cleaner for loading/error states)
  if (!message) return null;

  // 2. Ternary for binary states
  const icon = type === "error" ? <ErrorIcon /> : <InfoIcon />;

  // 3. && for optional elements (watch the falsy 0 trap!)
  // BAD: count && <Badge count={count} />  — renders "0" when count is 0
  // GOOD:
  const badge = count > 0 && <Badge count={count} />;

  // 4. Object map for multiple conditions
  const colorMap = {
    success: "green",
    warning: "orange",
    error: "red",
    info: "blue",
  };

  return (
    <div className={`notification notification--${type}`} style={{ borderColor: colorMap[type] }}>
      {icon}
      <span>{message}</span>
      {badge}
    </div>
  );
}

Lists and Keys

JSX
// Real-world: a data table with sorting and selection
function DataTable({ rows, columns }) {
  const [selected, setSelected] = useState(new Set());

  const toggleRow = (id) => {
    setSelected((prev) => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={col.key}>{col.label}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {rows.map((row) => (
          <tr
            key={row.id}               // always unique, stable ID
            className={selected.has(row.id) ? "row--selected" : ""}
            onClick={() => toggleRow(row.id)}
          >
            {columns.map((col) => (
              <td key={col.key}>{row[col.key]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Error Boundaries

Error boundaries catch JavaScript errors in any component below them in the tree.

JSX
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    // Log to error tracking (Sentry, Datadog, etc.)
    errorTracker.capture(error, {
      componentStack: info.componentStack,
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try Again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Wrap route sections, not individual components
function App() {
  return (
    <ErrorBoundary>
      <Header />
      <ErrorBoundary fallback={<DashboardError />}>
        <Dashboard />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}

Interview Questions Answered

Q: What is the difference between state and props?

Props are inputs passed from parent to child — immutable from the child's perspective. State is internal data managed by the component itself that can change over time, triggering re-renders.

Q: Why must React state updates be immutable?

React compares previous and next state by reference (===). If you mutate an object directly, the reference stays the same, React sees no change, and skips the re-render. Always return new objects/arrays.

Q: When does a component re-render?

A component re-renders when: (1) its own state changes, (2) its parent re-renders (and it's not memoized), (3) a context it consumes changes.

Q: What is the key prop for?

key is React's way to identify which elements changed between renders in a list. Stable, unique keys help React reuse DOM nodes efficiently rather than destroying and recreating them. Never use array index as key for lists that can reorder or filter.