React Tutorial

React is a JavaScript library for building user interfaces, developed by Meta (Facebook). It uses a component-based architecture and a virtual DOM for fast, declarative UI updates.

What is React?

  • Open-source library by Meta - NOT a full framework (just the UI layer).
  • Uses components - reusable, self-contained pieces of UI.
  • Uses a Virtual DOM - diffs changes and only updates what changed.
  • Declarative - you describe what the UI looks like, not how to change it.
  • Current version: React 18+ (concurrent features, automatic batching, Suspense).

Setup

Terminal
# Vite (fastest, recommended)
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev

# Create React App (legacy, slower)
npx create-react-app my-app
cd my-app
npm start

# Project structure (Vite)
my-app/
+-- src/
-   +-- main.jsx         ? entry point
-   +-- App.jsx          ? root component
-   +-- components/      ? reusable components
-   +-- assets/
+-- public/
+-- index.html
+-- vite.config.js
► Try It Yourself

JSX - JavaScript XML

JSX
// JSX is transpiled to React.createElement() calls
const element = <h1>Hello, World!</h1>;

// Expressions in JSX (curly braces)
const name = "Alice";
const el = <h1>Hello, {name}!</h1>;
const sum = <p>2 + 2 = {2 + 2}</p>;

// JSX differences from HTML
// class ? className,  for ? htmlFor
// Self-closing tags MUST close: <br />, <input />, <img />
// style takes an object with camelCase props
const style = <div style={{ backgroundColor: "red", fontSize: "18px" }}>...</div>;

// One root element (or Fragment)
return (
  <>
    <h1>Title</h1>
    <p>Paragraph</p>
  </>
);

// Comments in JSX
{/* This is a JSX comment */}
► Try It Yourself

Components

JSX
// Functional component (current standard)
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Arrow function component
const Card = ({ title, children }) => (
  <div className="card">
    <h2>{title}</h2>
    {children}
  </div>
);

// Use in parent
function App() {
  return (
    <>
      <Greeting name="Alice" />
      <Card title="Welcome">
        <p>This is card content</p>
      </Card>
    </>
  );
}

// Named exports (best practice)
export function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}
export default App;
► Try It Yourself

useState Hook

JSX
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);  // initial value = 0

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// Object state
function Profile() {
  const [user, setUser] = useState({ name: "Alice", age: 25 });

  const birthday = () =>
    setUser(prev => ({ ...prev, age: prev.age + 1 })); // spread old state!

  return <p>{user.name}, {user.age}</p>;
}

// Array state
function TodoList() {
  const [todos, setTodos] = useState(["Buy milk", "Code"]);

  const addTodo = (text) =>
    setTodos(prev => [...prev, text]);

  const removeTodo = (index) =>
    setTodos(prev => prev.filter((_, i) => i !== index));
}
► Try It Yourself

useEffect Hook

JSX
import { useState, useEffect } from "react";

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  // Run once when component mounts (empty deps [])
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users")
      .then(res => res.json())
      .then(data => { setUsers(data); setLoading(false); });
  }, []);  // ? dependency array = [] means "run once on mount"

  // Run when 'userId' changes
  const [userId, setUserId] = useState(1);
  useEffect(() => {
    fetch(`/api/users/${userId}`).then(...);
  }, [userId]);  // re-run when userId changes

  // Cleanup (unmount / before re-run)
  useEffect(() => {
    const timer = setInterval(() => console.log("tick"), 1000);
    return () => clearInterval(timer); // cleanup function
  }, []);

  if (loading) return <p>Loading...</p>;
  return (
    <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
  );
}
► Try It Yourself

useContext - Global State

JSX
import { createContext, useContext, useState } from "react";

// 1. Create context
const ThemeContext = createContext("light");

// 2. Provide context (wrap app or subtree)
function App() {
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Main />
    </ThemeContext.Provider>
  );
}

// 3. Consume context (any depth - no prop drilling)
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <header className={`header-${theme}`}>
      <button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>
    </header>
  );
}
► Try It Yourself

React Router v6

Terminal / JSX
npm install react-router-dom

// main.jsx
import { BrowserRouter } from "react-router-dom";
root.render(<BrowserRouter><App /></BrowserRouter>);

// App.jsx
import { Routes, Route, Link, NavLink, Navigate, useParams, useNavigate } from "react-router-dom";

function App() {
  return (
    <>
      <nav>
        <Link to="/">Home</Link>
        <NavLink to="/about" className={({isActive}) => isActive ? "active" : ""}>About</NavLink>
      </nav>

      <Routes>
        <Route path="/"          element={<Home />} />
        <Route path="/about"     element={<About />} />
        <Route path="/users/:id" element={<UserProfile />} />
        <Route path="*"          element={<Navigate to="/" />} />
      </Routes>
    </>
  );
}

// Access route params
function UserProfile() {
  const { id } = useParams();
  return <h1>User #{id}</h1>;
}

// Programmatic navigation
function LoginForm() {
  const navigate = useNavigate();
  const submit = () => navigate("/dashboard");
}
► Try It Yourself

Custom Hooks

JSX
// Custom hook: useFetch
function useFetch(url) {
  const [data,    setData]    = useState(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError]   = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(res => { if (!res.ok) throw new Error(res.status); return res.json(); })
      .then(data => { setData(data); setLoading(false); })
      .catch(err => { setError(err.message); setLoading(false); });
  }, [url]);

  return { data, loading, error };
}

// Use it in any component
function UserList() {
  const { data: users, loading, error } = useFetch("/api/users");
  if (loading) return <Spinner />;
  if (error)   return <ErrorMsg msg={error} />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// Custom hook: useLocalStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; }
    catch { return initialValue; }
  });
  const set = (v) => { setValue(v); localStorage.setItem(key, JSON.stringify(v)); };
  return [value, set];
}
► Try It Yourself

Component Architecture

As React apps grow, component boundaries matter more than syntax. Build small, focused components that own one clear job and receive data through props or context.

  • Keep page components responsible for composition and data fetching.
  • Keep presentational components focused on display and interaction.
  • Extract repeated logic into custom hooks instead of copying useEffect blocks.
  • Avoid deeply nesting state in one giant component.

State Flow

Students often struggle because React state feels invisible. Think in one direction: user action updates state, state updates UI.

React
function CartPage() {
  const [items, setItems] = useState([]);

  function handleRemove(id) {
    setItems(current => current.filter(item => item.id !== id));
  }

  return <CartTable items={items} onRemove={handleRemove} />;
}

Testing Components

React tests should verify behavior, not implementation details. Focus on what a user can see, click, and read.

React Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('increments counter', async () => {
  render(<Counter />);
  await userEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});