CSCI 4513
Week 12, Lecture 20
Bifröst connects different realms and guides travelers to their destinations, just as React Router connects different views and navigates users through your application.
The Tale:
Bifröst, the burning rainbow bridge, connects Midgard (the realm of humans) to Asgard (the realm of gods). This magnificent bridge glows with three colors and provides the primary pathway between the mortal and divine worlds. Without Bifröst, the gods would be isolated in Asgard, unable to easily interact with other realms or respond to events in the wider cosmos.
Heimdall, the watchman of the gods, stands eternal guard at Himinbjörg, his observatory at the bridge's Asgard end. Heimdall's senses are extraordinarily sharp—he can hear grass growing in the earth and see for hundreds of miles, day or night. He guards who crosses the bridge, ensures travelers reach their intended destinations, and sounds the Gjallarhorn when threats approach. The bridge always knows where each traveler should go and guides them appropriately.
// Bifröst - connecting different realms
const routes = [
{ path: "/", element: <Midgard /> },
{ path: "/asgard", element: <Asgard /> },
{ path: "/vanaheim", element: <Vanaheim /> }
];
// Heimdall - route guards
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
// Navigation - crossing the bridge
<Link to="/asgard">Cross to Asgard</Link>
// 404 - fell off Bifröst into void
errorElement: <FellIntoVoid />
What is a side effect?
Any interaction with things outside your component:
function Clock() {
const [counter, setCounter] = useState(0);
// ❌ This runs on EVERY render!
setInterval(() => {
setCounter(count => count + 1)
}, 1000);
return <p>{counter} seconds have passed.</p>;
}
// Creates infinite intervals - counter goes berserk!
Problem: setInterval runs on every render, creating multiple intervals that all update state, causing more renders!
import { useEffect, useState } from 'react';
function Clock() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const key = setInterval(() => {
setCounter(count => count + 1)
}, 1000);
// Cleanup function
return () => {
clearInterval(key);
};
}, []); // Empty dependency array
return <p>{counter} seconds have passed.</p>;
}
useEffect(
() => {
// Execute side effect here
return () => {
// Cleanup function (optional)
// Runs before next effect and on unmount
}
},
// Dependency array (optional)
[/* dependencies */]
)
// Runs after EVERY render
useEffect(() => {
console.log("Runs on every render");
});
// Runs only on mount (component appears)
useEffect(() => {
console.log("Runs once on mount");
}, []);
// Runs on mount AND when a or b change
useEffect(() => {
console.log("Runs when dependencies change");
}, [a, b]);
// ❌ Bad
const [sum, setSum] = useState(0);
useEffect(() => {
setSum(number1 + number2);
}, [number1, number2]);
// ✅ Good
const sum = number1 + number2;
Use onChange, onClick, etc. directly on elements
Traditional Multi-Page Apps (MPAs):
Single-Page Apps (SPAs) with Client-Side Routing:
Traditional Routing (MPA):
🍗 Put chicken in oven → ⏳ Wait → 🍽️ Eat
Forgot spices? Get up, add them, put it back, reheat, wait again...
Client-Side Routing (SPA):
🍗 Put chicken in oven → ⏳ Wait → 🍽️ Eat
Forgot spices? Use the microwave at the table!
You never have to leave your seat!
npm install react-router
Let's build a simple app with routing!
We'll create:
// App.jsx
const App = () => {
return (
<div>
<h1>Hello from the main page!</h1>
<p>Here are some links to other pages</p>
<nav>
<ul>
<li><a href="profile">Profile page</a></li>
</ul>
</nav>
</div>
);
};
// Profile.jsx
const Profile = () => {
return (
<div>
<h1>Hello from profile page!</h1>
<p>So, how are you?</p>
</div>
);
};
export default Profile;
// main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";
import App from "./App";
import Profile from "./Profile";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "profile",
element: <Profile />,
},
]);
createRoot(document.getElementById("root")).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);
1. Import createBrowserRouter and RouterProvider
2. Create router config as array of route objects
3. Each route has a path and element
4. Pass router to RouterProvider component
5. RouterProvider renders the matching element
Using regular anchor tags causes full page reloads!
import { Link } from "react-router";
const App = () => {
return (
<div>
<h1>Hello from the main page!</h1>
<nav>
<ul>
<li>
<Link to="profile">Profile page</Link>
</li>
</ul>
</nav>
</div>
);
};
Why nested routes?
Render a section of a page differently based on the URL
Example: /profile/popeye and /profile/spinach
// Popeye.jsx
import { Link } from "react-router";
const Popeye = () => {
return (
<>
<p>Hi, I am Popeye! I love to eat Spinach!</p>
<Link to="/">Click here to go back</Link>
</>
);
};
// Spinach.jsx
const Spinach = () => {
return (
<>
<p>Hi, I am Spinach! Popeye loves to eat me!</p>
<Link to="/">Click here to go back</Link>
</>
);
};
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "profile",
element: <Profile />,
children: [
{ path: "spinach", element: <Spinach /> },
{ path: "popeye", element: <Popeye /> },
],
},
]);
import { Outlet } from "react-router";
const Profile = () => {
return (
<div>
<h1>Hello from profile page!</h1>
<p>So, how are you?</p>
<hr />
<h2>The profile visited is here:</h2>
<Outlet />
</div>
);
};
<Outlet /> gets replaced by the child component based on the URL!
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "profile",
element: <Profile />,
children: [
{ index: true, element: <DefaultProfile /> },
{ path: "spinach", element: <Spinach /> },
{ path: "popeye", element: <Popeye /> },
],
},
]);
Index routes render when the parent path is visited with no additional path segments
const router = createBrowserRouter([
{
path: "/",
element: <App />,
},
{
path: "profile/:name",
element: <Profile />,
},
]);
:name is a dynamic segment - it matches any value in that position
import { useParams } from "react-router";
const Profile = () => {
const { name } = useParams();
return (
<div>
<h1>Hello from profile page!</h1>
<p>So, how are you?</p>
<hr />
<h2>The profile visited is here:</h2>
{name === "popeye" ? (
<Popeye />
) : name === "spinach" ? (
<Spinach />
) : (
<DefaultProfile />
)}
</div>
);
};
// ErrorPage.jsx
import { Link } from "react-router";
const ErrorPage = () => {
return (
<div>
<h1>Oh no, this route doesn't exist!</h1>
<Link to="/">
You can go back to the home page by clicking here!
</Link>
</div>
);
};
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
},
{
path: "profile/:name",
element: <Profile />,
},
]);
errorElement renders when:
// routes.jsx
import App from "./App";
import Profile from "./Profile";
import ErrorPage from "./ErrorPage";
const routes = [
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
},
{
path: "profile/:name",
element: <Profile />,
},
];
export default routes;
// main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";
import routes from "./routes";
const router = createBrowserRouter(routes);
createRoot(document.getElementById("root")).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);
Benefit: Can reuse routes array in test files!
// Parent component
import { Outlet } from "react-router";
const Parent = () => {
const [user, setUser] = useState({ name: "Alice" });
return (
<div>
<h1>Parent Component</h1>
<Outlet context={[user, setUser]} />
</div>
);
};
// Child component
import { useOutletContext } from "react-router";
const Child = () => {
const [user, setUser] = useOutletContext();
return (
<div>
<h2>Child Component</h2>
<p>User: {user.name}</p>
<button onClick={() => setUser({ name: "Bob" })}>
Change Name
</button>
</div>
);
};
import { useNavigate } from "react-router";
const LoginForm = () => {
const navigate = useNavigate();
const handleLogin = (e) => {
e.preventDefault();
// Perform login logic...
// Navigate to dashboard after successful login
navigate("/dashboard");
};
return <form onSubmit={handleLogin}>{/* ... */}</form>;
};
import { Navigate } from "react-router";
const ProtectedRoute = ({ isLoggedIn, children }) => {
if (!isLoggedIn) {
// Redirect to login page
return <Navigate to="/login" />;
}
return children;
};
<Navigate /> redirects when rendered
Great for protected routes!
Important: Components that use React Router must be tested within a routing context!
Two options:
import { render, screen } from "@testing-library/react";
import { createMemoryRouter, RouterProvider } from "react-router";
import routes from "./routes";
describe("App", () => {
it("renders home page", () => {
const router = createMemoryRouter(routes, {
initialEntries: ["/"],
});
render(<RouterProvider router={router} />);
expect(
screen.getByText(/hello from the main page/i)
).toBeInTheDocument();
});
});
const routes = [
{
path: "/",
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{ path: "about", element: <About /> },
{
path: "posts",
element: <Posts />,
children: [
{ index: true, element: <PostList /> },
{ path: ":postId", element: <PostDetail /> },
],
},
],
},
];
import { Outlet, Link } from "react-router";
const Layout = () => {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/posts">Posts</Link>
</nav>
<main>
<Outlet />
</main>
<footer>© 2025 My Blog</footer>
</div>
);
};
Layout Routes: Parent routes that only render <Outlet /> for shared UI
Index Routes: Default child route when parent path matches
Protected Routes: Routes that check authentication and redirect if needed
404 Routes: Catch-all routes for undefined paths
Nested Routes: Routes within routes for complex UIs
For your convenience, here's an excellent video tutorial:
📅 Next Class: Prop Types
📝 Homework: Memory Card Game Project