React State, Hooks & Routing
Hooks
Hooks are functions that let you use React state and lifecycle features from function components. They replace the need for class components.
Vue's Composition API (ref, computed, watch, onMounted) and React Hooks (useState, useMemo, useEffect) solve the same problem with different syntax.
Rules of hooks
- Only call hooks at the top level -- not inside loops, conditions, or nested functions
- Only call hooks from React function components or custom hooks -- not from regular functions
React relies on the order hooks are called to track state correctly. Breaking these rules causes bugs.
// WRONG -- hook inside condition
if (isLoggedIn) {
const [name, setName] = useState("");
}
// CORRECT -- condition inside hook logic
const [name, setName] = useState("");
if (isLoggedIn) {
// use name here
}
useState
Returns an array with the current state value and an updater function. Parameter is the initial value.
import { useState } from "react";
const Home = () => {
const [count, setCount] = useState(0);
return (
<>
<h1>Home {count}</h1>
<button onClick={() => setCount(count + 1)}>Add 1</button>
</>
);
};
export default Home;
You can use useState multiple times for different pieces of state:
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [isActive, setIsActive] = useState(false);
useState(0) is like const count = ref(0) in Vue. The difference: in Vue you access via count.value, in React you use the setter setCount(newValue). Never mutate state directly in React!
State immutability
Always create a new value when updating state. Never mutate the existing state directly:
// WRONG -- mutating state directly
const [items, setItems] = useState(["a", "b"]);
items.push("c"); // This does NOT trigger a re-render!
// CORRECT -- create new array
setItems([...items, "c"]);
// CORRECT -- for objects, spread and override
const [user, setUser] = useState({ name: "Andres", age: 25 });
setUser({ ...user, age: 26 });
useEffect
The Effect Hook adds the ability to perform side effects from a function component -- data fetching, subscriptions, DOM manipulation, timers, etc.
Dependency array controls when it runs
// Runs after EVERY render (rarely what you want)
useEffect(() => {
console.log("every render");
});
// Runs ONCE after first render (like Vue's onMounted)
useEffect(() => {
console.log("mounted");
}, []);
// Runs when count changes (like Vue's watch)
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
Missing the dependency array [] is a common bug that causes infinite re-renders when fetching data!
useEffect with cleanup
Return a function from useEffect to clean up when the component unmounts or before the effect re-runs:
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
// Cleanup: runs on unmount or before next effect
return () => clearInterval(timer);
}, []);
The cleanup function is like Vue's onUnmounted(). The difference: in React, cleanup also runs before each re-execution of the effect (not just on unmount).
useEffect -- data fetching example
import { useState, useEffect } from "react";
const Home = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
return (
<>
<h1>Home {count}</h1>
<button onClick={() => setCount(count + 1)}>Add 1</button>
</>
);
};
export default Home;
useRef
Returns a mutable ref object that persists across renders without causing re-renders.
Two main use cases:
1. DOM references (like Vue template refs)
import { useRef, useEffect } from "react";
const SearchInput = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder="Search..." />;
};
2. Mutable values that persist without re-rendering
const timerRef = useRef<number>(0);
useEffect(() => {
timerRef.current = window.setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timerRef.current);
}, []);
useRef for DOM access is like Vue's const inputRef = ref<HTMLInputElement>() with ref="inputRef" on the template element.
Data fetching with Axios
API service class (typed)
This follows the same pattern you used in Vue:
import Axios from "axios";
import { IGpsSession } from "../domain/IGpsSession";
export abstract class GpsSessionsApi {
private static axios = Axios.create({
baseURL: "https://sportmap.akaver.com/api/v1.0/GpsSessions/",
headers: {
common: {
"Content-Type": "application/json",
},
},
});
static async getAll(): Promise<IGpsSession[]> {
try {
const response = await this.axios.get<IGpsSession[]>("");
if (response.status === 200) {
return response.data;
}
return [];
} catch (error) {
console.log("error: ", (error as Error).message);
return [];
}
}
}
Fetching data in a component
Use useEffect with an empty dependency array [] to fetch data once on mount:
import { useState, useEffect } from "react";
import { IGpsSession } from "../domain/IGpsSession";
import { GpsSessionsApi } from "../services/GpsSessionApi";
const GpsSessions = () => {
const [items, setItems] = useState<IGpsSession[]>([]);
useEffect(() => {
const fetchData = async () => {
setItems(await GpsSessionsApi.getAll());
};
fetchData();
}, []); // Empty array = run once on mount
return (
<>
<h1>Gps Sessions</h1>
<table className="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Description</th>
<th>Recorded At</th>
<th>Locations</th>
<th>User</th>
</tr>
</thead>
<tbody>
{items.map((gpsSession) => (
<RowDisplay key={gpsSession.id} gpsSession={gpsSession} />
))}
</tbody>
</table>
</>
);
};
export default GpsSessions;
Row display as a separate component:
import { IGpsSession } from "../domain/IGpsSession";
const RowDisplay = ({ gpsSession }: { gpsSession: IGpsSession }) => (
<tr>
<td>{gpsSession.id}</td>
<td>{gpsSession.name}</td>
<td>{gpsSession.description}</td>
<td>{gpsSession.recordedAt}</td>
<td>{gpsSession.gpsLocationsCount}</td>
<td>{gpsSession.userFirstLastName}</td>
</tr>
);
Cleanup with AbortController
Cancel API requests when the component unmounts to avoid updating state on unmounted components:
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await Axios.get<IGpsSession[]>("/api/sessions", {
signal: controller.signal,
});
setItems(response.data);
} catch (error) {
if (!Axios.isCancel(error)) {
console.error("Fetch error:", error);
}
}
};
fetchData();
return () => controller.abort();
}, []);
Loading and error states
const GpsSessions = () => {
const [items, setItems] = useState<IGpsSession[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const result = await GpsSessionsApi.getAll();
setItems(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p className="text-danger">Error: {error}</p>;
return (
// ... table rendering
);
};
Custom hooks
Custom hooks extract reusable stateful logic into functions -- React's equivalent of Vue composables.
A custom hook is a function that starts with use and can call other hooks:
import { useState, useEffect } from "react";
import Axios from "axios";
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await Axios.get<T>(url, {
signal: controller.signal,
});
setData(response.data);
} catch (err) {
if (!Axios.isCancel(err)) {
setError(err instanceof Error ? err.message : "Error");
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
export default useFetch;
Usage:
const GpsSessions = () => {
const { data, loading, error } = useFetch<IGpsSession[]>(
"https://sportmap.akaver.com/api/v1.0/GpsSessions/"
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data?.map((session) => (
<li key={session.id}>{session.name}</li>
))}
</ul>
);
};
Custom hooks work just like Vue composables. Compare useFetch<T>(url) in React to useApi<T>(url) in Vue -- same concept, same naming convention.
Next.js App Router -- Routing
Next.js uses the file system for routing. Folders define routes, and page.tsx files make them accessible.
Folder-based routing
src/app/
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── dashboard/
│ ├── layout.tsx # Shared layout for /dashboard/*
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
Layouts
A layout wraps pages and nested layouts. It preserves state across navigations (does not re-render).
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>{/* Sidebar */}</nav>
{children}
</section>
);
}
- Only the root layout can contain
<html>and<body>tags - When
layout.tsxandpage.tsxexist in the same folder, the layout wraps the page - Metadata can be exported from any
layout.tsxorpage.tsx(child metadata merges with and overrides parent metadata)
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Dashboard",
description: "Your dashboard",
};
Linking and navigation
<Link> component (preferred for navigation):
import Link from "next/link";
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
Active link detection with usePathname():
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export function NavLinks() {
const pathname = usePathname();
return (
<ul className="navbar-nav">
<li className="nav-item">
<Link
className={`nav-link ${pathname === "/" ? "active" : ""}`}
href="/"
>
Home
</Link>
</li>
<li className="nav-item">
<Link
className={`nav-link ${pathname === "/about" ? "active" : ""}`}
href="/about"
>
About
</Link>
</li>
</ul>
);
}
Programmatic navigation with useRouter():
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/dashboard")}>
Dashboard
</button>
);
}
Component hierarchy
Next.js provides special files for common UI patterns in each route segment:
| File | Purpose |
|---|---|
layout.tsx | Shared UI for a segment and its children |
page.tsx | Unique UI of a route |
loading.tsx | Loading UI (shown while page loads) |
not-found.tsx | 404 UI |
error.tsx | Error UI (catches runtime errors) |
template.tsx | Re-rendered layout (unlike layout, re-renders on navigation) |

Only page.tsx and route.tsx are publicly addressable.
Dynamic routes
Create dynamic segments by wrapping a folder name in square brackets:
src/app/blog/[slug]/page.tsx → /blog/my-post
src/app/users/[id]/page.tsx → /users/123
Access the parameter via props:
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function BlogPost({ params }: PageProps) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}
Catch-all segments with [...slug]:
src/app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
Optional catch-all with [[...slug]]:
src/app/shop/[[...slug]]/page.tsx → /shop, /shop/a, /shop/a/b
Next.js folder-based routing is similar to Nuxt's file-based routing. Dynamic segments [id] correspond to Vue Router's :id parameter. The Link component is like Vue Router's <RouterLink>.
Shared state
- Pass state down via props (immutable in child component)
- Include callbacks to update parent's state from child component
- This gets complicated quickly -- "Prop Drilling" and callback hell
- Solutions: Context, useReducer, Zustand/Redux -- covered in lectures 15-16