Skip to main content

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 comparison

Vue's Composition API (ref, computed, watch, onMounted) and React Hooks (useState, useMemo, useEffect) solve the same problem with different syntax.

Rules of hooks

  1. Only call hooks at the top level -- not inside loops, conditions, or nested functions
  2. 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);
Vue comparison

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]);
danger

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);
}, []);
Vue comparison

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);
}, []);
Vue comparison

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>
);
};
Vue comparison

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.tsx and page.tsx exist in the same folder, the layout wraps the page
  • Metadata can be exported from any layout.tsx or page.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:

FilePurpose
layout.tsxShared UI for a segment and its children
page.tsxUnique UI of a route
loading.tsxLoading UI (shown while page loads)
not-found.tsx404 UI
error.tsxError UI (catches runtime errors)
template.tsxRe-rendered layout (unlike layout, re-renders on navigation)

Component hierarchy

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
Vue comparison

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