ECE1724: Advanced Web Development

React Ecosystem and Modern Frameworks

Chen Ying

Assistant Professor, Teaching Stream

Department of Electrical and Computer Engineering

University of Toronto

Last Week’s Lecture

React: Declarative, component-based JavaScript library for building user interfaces

  • Focuses on what the UI should look like, not how to update it
  • Efficiently updates the UI using a Virtual DOM

Create a React Project

Use Vite: A modern build tool for React

npm create vite@latest my-app

Create a TypeScript React project

npm create vite@latest my-app -- --template react-ts

Create React App vs. Vite

Create React App

  • Simple, zero-configuration starting point

Vite

  • Much faster startup and hot reload
  • Simpler internal tooling
  • Widely used in current industry React projects

Last Week’s Lecture

Core React Concepts

  • Components: Reusable building blocks of a React app

  • Props: Read-only data passed from parent components to child components

    App.tsx
    type WelcomeProps = {
      name: string;
    };
    
    function Welcome({ name }: WelcomeProps) {
      return <h1>Welcome to {name}'s Lecture!</h1>;
    }
    
    function App() {
      return <Welcome name="Chen" />;
    }

Core React Concepts

Components: Reusable building blocks of a React app

Props: Read-only data passed from parent components to child components

State: Local, mutable data managed within a component that triggers UI updates

App.tsx and main.tsx

App.tsx: Root component

  • Defines the top-level UI structure
  • Renders child components
  • Serves as the container for the entire application

main.tsx: Application entry point

  • Imports the root React component (App.tsx)
  • Creates a React root and mounts the app into the DOM

index.html

HTML entry point

my-app/index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>my-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

index.html

HTML entry point

my-app/index.html
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
  • <div id="root"></div>: The mount point — React renders the entire app into this element
  • <script type="module" src="/src/main.tsx"></script>: Loads the entry file (main.tsx)

HMR

Hot Module Replacement: A development tool provided by Vite

  • Updates the app instantly when you edit code
  • No full page reload
  • App state is preserved when possible
  • Speeds up development and debugging

Today’s Lecture

JSX/TSX Syntax

  • JSX/TSX Rules

JSX & TSX

JSX (JavaScript XML): Syntax extension for JavaScript that allows you to write HTML-like code within JavaScript

  • Combines power of JavaScript + readability of HTML

TSX: JSX inside TypeScript files

type WelcomeProps = {
  name: string;
};

function Welcome({ name }: WelcomeProps) {
  return <h1>Welcome to {name}'s Lecture!</h1>;
}

Rule 1: One Parent Element

JSX must return one parent element

export default function App() {
  return (
    <>
      <h1>Hello</h1>
      <p>React + TypeScript</p>
    </>
  );
}

Not allowed:

export default function App() {
  return (
    <h1>Hello</h1>
    <p>React + TypeScript</p>
  );
}
  • Use <div>...</div>, <main></main>, <>...</>

Rule 2: Different Attributes

HTML Attributes: Additional information about an element

<img src="profile.jpeg" alt="Profile Picture" class="profile-pic" />


JSX Attributes: May different from regular HTML attributes

const profilePic = (
  <img src="profile.jpeg" alt="Profile Picture" className="profile-pic" />
);
  • Use camelCase
  • Examples: className (not class), htmlFor (not for)

Rule 2: Different Attributes

HTML: for and id link the label to that specific form control

<form class="form">
  <label for="username">Username: </label>
  <input id="username" type="text" />
</form>

JSX

export default function UserForm() {
  return (
    <form className="form">
      <label htmlFor="username">Username: </label>
      <input id="username" type="text" />
    </form>
  );
}

Rule 3: Self-Closing Tags

In HTML, self-closing tags can omit the slash

<div>
  <img src="logo.png" alt="App Logo">
  <br>
</div>

In JSX, self-closing tags must have a closing slash

const example = (
  <div>
    <img src="logo.png" alt="App Logo" />
    <br />
  </div>
);

Recap: JSX/TSX Rules

Rule 1: One Parent Element

  • JSX/TSX must return one parent element
  • Use <div>...</div>, <main></main>, <>...</>

Rule 2: Different Attributes

  • JSX attributes may different from HTML attributes
  • Examples: className (not class), htmlFor (not for)

Rule 3: Self-Closing Tags

  • Must have a closing slash (e.g., <br />)

Today’s Lecture

JSX/TSX Syntax

  • JSX/TSX Rules
  • JavaScript/TypeScript inside JSX/TSX

Embedding Expressions

Use curly braces {} to embed any JavaScript/TypeScript expression

const name = "Alice";

export default function Greeting() {
  return <h1>Hello, {name.toUpperCase()}!</h1>;
}

Output?

export default function App() {
  return (
    <>
      <h1>{2 + 2}</h1>
      <h1>2 + 2</h1>
    </>
  );
}

JS/TS inside JSX/TSX

Dynamic Logic

Rendering Lists

Event Handlers

Dynamic Logic

src/App.tsx
function App() {
  let isLoggedIn = true;

  isLoggedIn = false;

  return <h1>{isLoggedIn ? "Welcome Back!" : "Please Log In"}</h1>;
}

export default App;

Rendering Lists

Use JavaScript methods (e.g., map()) to render lists dynamically

src/components/ListExample.tsx
const items = ["React", "TSX", "Hooks"];

export default function ListExample() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

key

  <ul>
    {items.map((item) => (
      <li key={item}>{item}</li>
    ))}
  </ul>

key is a special prop used by React

  • Gives each list item a stable identity
  • Must be unique among siblings

key lets React

  • Update only what changed (efficient rendering)
  • Preserve component state when lists change

key

  <ul>
    {items.map((item) => (
      <li key={item}>{item}</li>
    ))}
  </ul>

key is used internally by React
It is not rendered to the DOM

Rendering List

src/components/ListExample.tsx
const items = ["React", "TSX", "Hooks"];

export default function ListExample() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

What the browser actually sees:

<ul>
  <li>React</li>
  <li>TSX</li>
  <li>Hooks</li>
</ul>

Live Demo

src/App.tsx
import ListExample from "./components/ListExample";

function App() {
  return <ListExample />;
}

export default App;

JS/TS inside JSX/TSX

Dynamic Logic

Rendering Lists

Event Handlers

Event Handlers

JavaScript/TypeScript functions can be passed as event handlers in JSX/TSX

src/components/ButtonExample.tsx
export default function ButtonExample() {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    alert("Button clicked!");
    console.log("clicked", e.currentTarget);
  };

  return (
    <button type="button" onClick={handleClick}>
      Click Me
    </button>
  );
}

Event Type

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  alert("Button clicked!");
  console.log("clicked", e.currentTarget);
};
  • MouseEvent: This handler responds to a mouse interaction (click, double-click, etc.)
  • HTMLButtonElement: The event came from a <button> element

TypeScript now knows e.currentTarget is a button

  • Button-specific properties are safe to use
  • Mistakes are caught at compile time

Event Handler

src/components/ButtonExample.tsx
export default function ButtonExample() {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    alert("Button clicked!");
    console.log("clicked", e.currentTarget);
  };

  return (
    <button type="button" onClick={handleClick}>
      Click Me
    </button>
  );
}

React.MouseEvent<HTMLButtonElement> means a mouse event coming from a button

React uses its own typed event system (React.*Event)

Live Demo

src/App.tsx
import ButtonExample from "./components/ButtonExample";

function App() {
  return <ButtonExample />;
}

export default App;

Recap: JSX/TSX Syntax

  • HTML-Like Syntax in JavaScript: Combine the structure of HTML with the logic of JavaScript for a declarative UI
  • Embedding JavaScript/TypeScript: Use {} to dynamically insert variables, expressions, function calls
  • Single Parent Element: Wrap all elements inside a single parent container (e.g. <> and </>)
  • Attributes: Use camelCase (e.g. className, htmlFor)
  • Self-Closing Tags: Must have a closing slash (e.g., <br />)

Today’s Lecture

JSX/TSX Syntax

Hooks

Hooks

Functions that let you use React’s features (e.g. state) in function components

src/components/Counter.tsx
import { useState } from "react";

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

  return (
    <div>
      <p>Current Count: {count}</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

Hooks

Functions that let you use React’s features (e.g. state) in function components

  • Simplify React development
  • Widely used in modern React applications

useSuperPower()

  • useState, useEffect, useContext, …

One Rule

Only call hooks at the top level of a function component

function App() {
  // This works
  useHook();

  // This won't work
  const oops = () => {
    useHook();
  };

  // This won't work
  return <button onClick={() => useHook()}></button>;
}

Only exception: Custom hooks

Basic Hooks

  • useState
  • useEffect
  • useContext

useState

Re-render the UI when state changes


Syntax

const [state, setState] = useState(initialValue);
  • state: Current state value
  • setState: Function to update the state

useState: Example

src/components/Counter.tsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState<number>(0);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <p>Current Count: {count}</p>
      <button type="button" onClick={increment}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

TypeScript usually infers state type from initial value

const [count, setCount] = useState(0);

When You Need a Type

If initial value is null or an empty array/object, add a generic

  • <T>: A type parameter that allows to store any type while keeping type safety
import { useState } from "react";

type User = { id: number; name: string };

export default function Example() {
  const [user, setUser] = useState<User | null>(null);
  // ...
  return <div>{user ? user.name : "Loading..."}</div>;
}

Live Demo

App.tsx
import Counter from "./components/Counter";

function App() {
  return <Counter />;
}

export default App;

Can This Work?

src/components/Counter.tsx
function Counter() {
  let count = 0;

  const increment = () => {
    count += 1;
  };

  return (
    <div>
      <p>Current Count: {count}</p>
      <button type="button" onClick={increment}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

“Super Power” of useState

When setCount(count + 1) is called, React detects a state update and re-runs the Counter function (re-renders)

src/components/Counter.tsx
import { useState } from "react";

function Counter() {
  const [count, setCount] = useState<number>(0);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <p>Current Count: {count}</p>
      <button type="button" onClick={increment}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

Share State Between Components

src/App.tsx
import Counter from "./components/Counter";

function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

export default App;

Lift State Up

Keep state in the nearest common parent (e.g., App), pass values + handlers down (e.g., AppCounter)

Lift State Up: Step 1

Move the state up from Counter to App

src/App.tsx
import { useState } from "react";
import Counter from "./components/Counter";

function App() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  return (
    <>
      <Counter />
      <Counter />
    </>
  );
}

export default App;

Lift State Up: Step 2

Pass the state down from App to each Counter, together with the shared handler

src/App.tsx
import { useState } from "react";
import Counter from "./components/Counter";

function App() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return (
    <>
      <Counter count={count} onClick={increment} />
      <Counter count={count} onClick={increment} />
    </>
  );
}

export default App;

Lift State Up: Step 3

Change Counter to read the props passed from its parent component

src/components/Counter.tsx
type CounterProps = {
  count: number;
  onClick: () => void;
};

function Counter(props: CounterProps) {
  return (
    <div>
      <p>Current Count: {props.count}</p>
      <button type="button" onClick={props.onClick}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

Props Type

Destructure

src/components/Counter.tsx
type CounterProps = {
  count: number;
  onClick: () => void;
};

function Counter({ count, onClick }: CounterProps) {
  return (
    <div>
      <p>Current Count: {count}</p>
      <button type="button" onClick={onClick}>
        Increment
      </button>
    </div>
  );
}

export default Counter;

Live Demo

src/App.tsx
import { useState } from "react";
import Counter from "./components/Counter";

function App() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return (
    <>
      <Counter count={count} onClick={increment} />
      <Counter count={count} onClick={increment} />
    </>
  );
}

export default App;

Lift State Up

Each Counter’s onClick prop was set to the increment function inside App; count prop was set to 0

When a Counter’s button is clicked, the onClick handler fires, and the code inside of increment runs

increment calls setCount(count + 1), incrementing the count state variable

The new count value is passed as a prop to each Counter, so they show the same new value

Basic Hooks

useState: Manage state within a component

  • Re-render UI when state changes

useEffect

Component Lifecycle

Describes how a component behaves over time

  • From the moment it appears on the screen
  • To the moment it is removed from the DOM

Lifecycle Phases

A React component goes through three main phases

  1. Mounting: The component is created and added to the DOM
  2. Updating: The component re-renders when state or props change
  3. Unmounting: The component is removed from the DOM

These phases happen automatically

  • React manages the lifecycle for you

React’s Role

React renders the UI declaratively

  • You describe what the UI should look like
  • React figures out how to update the DOM

However, not all logic is about rendering UI

Side Effects

Operations that go beyond rendering the UI

  • Communicating with external systems
  • Fetching data from APIs
  • Setting up timers, listeners, subscriptions, …

These operations must be coordinated with the component lifecycle

Assignment 3

Develop a React-based user interface for managing papers and authors

  • Fetch data from Assignment 2 backend API
  • Display and update data based on user interactions

Side Effects & Lifecycle

React allows you to tie side effects to specific lifecycle phases

  • Mounting ← Fetch initial data
  • Updating ← Change when state or props update
  • Unmounting ← Clean up resources (e.g., timer) to avoid memory leaks

useEffect

Handle side effects

  • Run code at specific points in a component’s lifecycle

With useEffect, you can:

  • Run code after the component mounts
  • Re-run code when state or props change
  • Clean up resources when the component unmounts

useEffect: Syntax

useEffect(() => {
  // Code runs when the component mounts
  // and when dependencies change

  return () => {
    // Cleanup code runs before the component unmounts
    // or before the effect re-runs due to dependency changes
  };
}, [dependencies]);
  • Dependency array:
    • If empty ([]): Run only on mount, cleanup on unmount
    • If specific values ([value1, value2]): Run when the dependencies change

Dependency Array

  • State variables
  • Props (values passed from a parent component)
import { useEffect, useState } from "react";

function ChatRoom() {
  // State that controls which chat room is active
  const [roomId, setRoomId] = useState("general");

  useEffect(() => {
    // Side effect: connect to the chat room
    const connection = createConnection(roomId);
    connection.connect();

    // Cleanup: disconnect from the previous room
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // Re-run effect when roomId changes

  return (
    <div>
      <p>Current room: {roomId}</p>

      <button onClick={() => setRoomId("general")}>General</button>
      <button onClick={() => setRoomId("random")}>Random</button>
    </div>
  );
}

Side Effect: Timer

scr/components/Timer.tsx
import { useEffect, useState } from "react";

export function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
      console.log("Timer unmounted");
    };
  }, []);

  return <h1>Elapsed Time: {seconds} seconds</h1>;
}

[]: Effect runs once on mount

Live Demo

scr/App.tsx
import { Timer } from "./components/Timer";

function App() {
  return <Timer />;
}

export default App;

Side Effects: Benefits

  • Beyond Rendering: Side effects handle operations outside React’s main rendering process
  • Predictable Execution: useEffect ensures side effects are explicitly and reliably managed
  • Lifecycle Integration: React ties side effects to component lifecycles (mount, update, unmount)

Basic Hooks

useState: Manage state within a component

useEffect: Handle side effects and lifecycle-related logic

useContext: Share values across components without passing props manually

Why useContext?

Passing props through many layers can become

  • Verbose
  • Error-prone
  • Hard to maintain

useContext allows components to access shared data directly, no matter how deeply nested they are

How useContext Works

  1. Create a context
  2. Provide a context value
  3. Consume the value in a component

Create a Context

Use createContext() to define a context and its default value

src/contexts/UserContext.tsx
import { createContext } from "react";

const UserContext = createContext("Guest");

export default UserContext;
  • "Guest" is the default value
    • Used only if no provider is found

Provide a Context Value

A Provider makes the context value available to all child components

src/App.tsx
import WelcomeMessage from "./components/WelcomeMessage";
import UserContext from "./contexts/UserContext";

function App() {
  return (
    <UserContext.Provider value="Alice">
      <WelcomeMessage />
    </UserContext.Provider>
  );
}

value is the data shared with consuming components

  • Any descendant can access it

Use Context in a Component

useContext() lets a component read the context value

src/components/WelcomeMessage.tsx
import { useContext } from "react";
import UserContext from "../contexts/UserContext";

function WelcomeMessage() {
  const user = useContext(UserContext);

  return <h1>Welcome, {user}!</h1>;
}

export default WelcomeMessage;

Live Demo

src/App.tsx
import WelcomeMessage from "./components/WelcomeMessage";
import UserContext from "./contexts/UserContext";

function App() {
  return (
    <UserContext.Provider value="Alice">
      <WelcomeMessage />
    </UserContext.Provider>
  );
}

export default App;

useContext Key Points

function WelcomeMessage() {
  const user = useContext(UserContext);

  return <h1>Welcome, {user}!</h1>;
}
  • useContext() returns the current context value
  • React looks for the closest Provider above the component
  • If no Provider exists, the default value is used
  • When the context value changes, all consuming components re-render automatically

Basic Hooks

useState: Manage state within a component

useEffect: Handle side effects and lifecycle-related logic

useContext: Share values globally without passing props manually

Built-in React Hooks

https://react.dev/reference/react/hooks

Custom Hook

A reusable function that:

  • Encapsulates logic
  • Uses one or more React hooks
  • Can be shared across components

Custom Hook: Example

useToggle: Manages a boolean state and provides a toggle function

src/hooks/useToggle.tsx
import { useState } from "react";

function useToggle(initialValue: boolean): [boolean, () => void] {
  const [value, setValue] = useState(initialValue);

  const toggle = () => {
    setValue((prev) => !prev);
  };

  return [value, toggle];
}

export default useToggle;

Custom Hook: Example

ToggleButton component uses useToggle

src/components/ToggleButton.tsx
import useToggle from "../hooks/useToggle";

function ToggleButton() {
  const [isOn, toggleIsOn] = useToggle(false);

  return (
    <>
      <p>The button is: {isOn ? "ON" : "OFF"}</p>
      <button type="button" onClick={toggleIsOn}>
        Toggle
      </button>
    </>
  );
}

export default ToggleButton;

Live Demo

scr/App.tsx
import ToggleButton from "./components/ToggleButton";

function App() {
  return <ToggleButton />;
}

export default App;

Recap: Hooks

Functions that allow components to manage state, side effects, and lifecycle features

  • useState: Manage component state
  • useEffect: Handle side effects and lifecycle logic
  • useContext: Share values globally without passing props manually
  • Custom Hook: Reusable function that encapsulates logic using hooks

Lecture Summary

JSX/TSX Syntax

  • One parent element
  • JSX-specific attributes (className, htmlFor)
  • Self-closing tags
  • Embedding JavaScript/TypeScript with {}

Hooks

  • React built-in hooks (useState, useEffect, useContext)
  • Custom hooks