Side Effects & Routing

Side Effects & Routing

useEffect and React Router

CSCI 4513

Week 12, Lecture 20

Today's Learning Objectives

Norse Mythology Connection

Bifröst - The Rainbow Bridge

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.

Connecting the Realms

// 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 />

Reflection Questions

Before We Route: useEffect

Dealing with Side Effects

What is a side effect?

Any interaction with things outside your component:

  • Fetching data from a server
  • Setting up timers or intervals
  • Subscribing to events
  • Manipulating the DOM directly

The Problem Without useEffect

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!

The useEffect Hook

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 Anatomy

useEffect(
  () => {
    // Execute side effect here

    return () => {
      // Cleanup function (optional)
      // Runs before next effect and on unmount
    }
  },
  // Dependency array (optional)
  [/* dependencies */]
)

Dependency Array Patterns

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

When NOT to Use useEffect

❌ Don't use for calculations

// ❌ Bad
const [sum, setSum] = useState(0);
useEffect(() => {
  setSum(number1 + number2);
}, [number1, number2]);

// ✅ Good
const sum = number1 + number2;

❌ Don't use for event handlers

Use onChange, onClick, etc. directly on elements

Client-Side Routing

Building Multi-Page Apps Without Page Reloads

Traditional Multi-Page Apps (MPAs):

  • Browser requests new HTML for each page
  • Full page reload on every navigation
  • Server handles all routing

Single-Page Apps (SPAs) with Client-Side Routing:

  • JavaScript intercepts navigation
  • No page reload - just swap components
  • Faster, app-like experience

The Cooking Analogy

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!

Why React Router?

Installing React Router

npm install react-router

Let's build a simple app with routing!

We'll create:

  • A home page (App.jsx)
  • A profile page (Profile.jsx)
  • Routing configuration

Create Basic Pages

// 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>
  );
};

Create Basic Pages (cont.)

// Profile.jsx
const Profile = () => {
  return (
    <div>
      <h1>Hello from profile page!</h1>
      <p>So, how are you?</p>
    </div>
  );
};

export default Profile;

Setting Up the Router

// 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>
);

How the Router Works

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

The Link Component

Problem with <a> tags

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

Nested Routes

Routes Within Routes

Why nested routes?

Render a section of a page differently based on the URL

Example: /profile/popeye and /profile/spinach

Creating Child Components

// 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>
    </>
  );
};

Nested Route Configuration

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
  },
  {
    path: "profile",
    element: <Profile />,
    children: [
      { path: "spinach", element: <Spinach /> },
      { path: "popeye", element: <Popeye /> },
    ],
  },
]);

The Outlet Component

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!

Index Routes

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

Dynamic Segments (URL Params)

Making Routes Dynamic

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
  },
  {
    path: "profile/:name",
    element: <Profile />,
  },
]);

:name is a dynamic segment - it matches any value in that position

Using URL Params with useParams

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

Handling Bad URLs

// 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>
  );
};

Adding Error Elements

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    errorElement: <ErrorPage />,
  },
  {
    path: "profile/:name",
    element: <Profile />,
  },
]);

errorElement renders when:

  • Route doesn't exist
  • Component throws an error

Refactoring Routes

// 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;

Importing 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!

Passing Data Through Outlets

// 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>
  );
};

Using Outlet Context

// 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>
  );
};

Programmatic Navigation

Navigate Without Link Components

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>;
};

The Navigate Component

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!

Testing Components with Router

Important: Components that use React Router must be tested within a routing context!

Two options:

  • MemoryRouter: For simple components with Links
  • createMemoryRouter: For components that rely on router features (params, outlets, etc.)

Testing Example

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();
  });
});

Practical Example: Blog App

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 /> },
        ],
      },
    ],
  },
];

Blog App Layout

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

React Router Best Practices

Common Patterns

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

Want to Learn More?

React Router Video Tutorial

For your convenience, here's an excellent video tutorial:

Review: Key Concepts

Today's Takeaways

You Can Now:

📅 Next Class: Prop Types

📝 Homework: Memory Card Game Project