Week 13: React Router & Advanced Hooks

Building Production-Ready SPAs

INFO 153A/253A - Front-End Web Architecture

UC Berkeley

Topics: React Router v6, useRef, useMemo, useCallback, Custom Hooks

Part 1: Prep Work Recap

Reviewing React Router & Advanced Hooks

React Router Evolution

  • React Router v5 → v6 migration: Major API improvements for cleaner code
  • Switch replaced by Routes: More intuitive component nesting
  • component prop → element prop: Now accepts JSX directly
  • Exact matching by default: No more exact prop needed
  • Relative paths: Better support for nested routing
Key Takeaway: React Router v6 simplifies routing with more intuitive APIs and better TypeScript support

Advanced Hooks Overview

  • useRef: Access DOM elements and persist values without re-renders
  • useMemo: Memoize expensive computations
  • useCallback: Prevent function recreation on every render
  • Custom hooks: Extract and reuse stateful logic
  • Performance focus: Optimize only when necessary
Common Mistake: Over-optimization with useMemo/useCallback can actually harm performance

Why These Topics Matter

  • SPAs need routing: Users expect URL changes and browser history
  • Performance at scale: Advanced hooks prevent unnecessary work
  • Code organization: Custom hooks enable reusable logic
  • Professional patterns: These are standard in production React
  • Interview topics: Frequently asked about in technical interviews

Part 2: React Router Deep Dive

Building Modern Single-Page Applications

Setting Up React Router v6

import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/blog/:id" element={<BlogPost />} />
      </Routes>
    </BrowserRouter>
  );
}
  • BrowserRouter wrapper: Enables routing for entire app
  • Routes container: Groups all route definitions
  • Element prop with JSX: Direct component rendering
  • Path patterns: Static, dynamic, and nested routes
  • Automatic exact matching: No ambiguity in route selection

Navigation Components

Link vs NavLink

// Basic navigation
<Link to="/about">About</Link>

// Active styling
<NavLink
  to="/about"
  className={({ isActive }) =>
    isActive ? 'active' : ''
  }
>
  About
</NavLink>
  • Link: Basic navigation without page reload
  • NavLink: Adds active state styling
  • Function className: Dynamic styling based on route
  • No anchor tags: Prevents full page refreshes

Dynamic Routes with useParams

// Route definition
<Route path="/blog/:id" element={<BlogPost />} />

// Component using params
function BlogPost() {
  const { id } = useParams();
  const [post, setPost] = useState(null);

  useEffect(() => {
    fetchBlogPost(id).then(setPost);
  }, [id]);

  return <article>{post?.title}</article>;
}
  • :id parameter: Dynamic segment in URL path
  • useParams hook: Extracts URL parameters as object
  • Effect dependency: Re-fetch when ID changes
  • Real-world pattern: Common for detail pages

Programmatic Navigation

import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      navigate('/dashboard');  // Redirect after login
    } catch (error) {
      console.error('Login failed');
    }
  };

  return (/* form JSX */);
}
  • useNavigate hook: Returns navigation function
  • Imperative navigation: Navigate based on logic
  • Common patterns: After form submission, authentication
  • Replace option: navigate('/path', { replace: true })
  • Go back: navigate(-1) for browser back button

Nested Routes & Outlets

// Parent route with nested children
<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<Overview />} />
  <Route path="analytics" element={<Analytics />} />
  <Route path="settings" element={<Settings />} />
</Route>

// DashboardLayout component
function DashboardLayout() {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>
        <Outlet />  {/* Renders nested route */}
      </main>
    </div>
  );
}
  • Layout persistence: Sidebar stays while content changes
  • Outlet component: Placeholder for nested routes
  • Index route: Default child route
  • URL structure: /dashboard/analytics

Part 3: useRef Hook Mastery

Direct DOM Access & Value Persistence

Understanding useRef

Core Concept: useRef creates a mutable reference that persists across renders without triggering re-renders
  • Two main uses: DOM references and mutable values
  • Returns object: { current: value }
  • Persists between renders: Unlike regular variables
  • No re-render on change: Unlike useState
  • Escape hatch: For imperative DOM operations

DOM Element References

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Focus input on mount
    inputRef.current?.focus();
  }, []);

  const selectAll = () => {
    inputRef.current?.select();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={selectAll}>Select All</button>
    </div>
  );
}
  • ref attribute: Connects ref to DOM element
  • current property: Actual DOM node
  • Imperative methods: focus(), select(), scrollIntoView()
  • Null checking: Element might not exist yet
  • Common uses: Focus management, animations, measurements

Storing Previous Values

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;  // Store after render
  });

  const prevCount = prevCountRef.current ?? 0;

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </div>
  );
}
  • Effect without deps: Runs after every render
  • Timing matters: Previous value updated after render
  • No infinite loop: Ref changes don't trigger renders
  • Use case: Comparing values, animations, undo features

Memory Leak Prevention

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

  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (/* timer UI */);
}
  • Store interval ID: Need reference for cleanup
  • Cleanup function: Prevents memory leaks
  • Component unmount: Cleanup runs automatically
  • Pattern: Store any cleanup-needed values

Part 4: Performance Optimization

useMemo & useCallback for Efficient React

Understanding Memoization

Definition: Caching results of expensive operations to avoid recalculation
  • Problem: Components re-render → functions re-run
  • Solution: Cache results when inputs don't change
  • Trade-off: Memory for computation time
  • When to use: Profiler shows performance issues
  • When NOT to use: Premature optimization

useMemo for Expensive Computations

function DataGrid({ data, filter }) {
  // Expensive filtering operation
  const filteredData = useMemo(() => {
    console.log('Filtering data...');
    return data.filter(item =>
      item.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [data, filter]);  // Only re-compute when these change

  return (
    <table>
      {filteredData.map(item => (
        <tr key={item.id}>
          <td>{item.name}</td>
        </tr>
      ))}
    </table>
  );
}
  • Dependency array: Re-compute only when deps change
  • Skips computation: Returns cached value if deps same
  • Good for: Complex calculations, data transformations
  • Not for: Simple operations, reference equality
  • Debug tip: Console.log shows when it runs

useCallback for Function Memoization

function TodoList({ todos }) {
  const [filter, setFilter] = useState('all');

  // Memoized callback
  const deleteTodo = useCallback((id) => {
    console.log('Creating delete function');
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);  // Empty deps - function never changes

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={deleteTodo}  // Same reference
        />
      ))}
    </div>
  );
}
  • Problem solved: Prevents child re-renders
  • Reference equality: Same function object
  • React.memo benefit: Child skips render if props same
  • Event handlers: Common use case
  • Dependencies: Include all used values

When to Optimize

✅ Good Uses

  • Heavy computations (sorting, filtering large arrays)
  • Complex calculations (cryptography, statistics)
  • Preventing child re-renders
  • Referential equality needs

❌ Avoid When

  • Simple operations
  • Primitive dependencies
  • Infrequent re-renders
  • No measured performance issue
Remember: Wrong memoization can make performance worse by adding overhead

Part 5: Custom Hooks

Building Reusable Logic Patterns

Why Custom Hooks?

  • Code reuse: Share stateful logic between components
  • Separation of concerns: Extract complex logic
  • Testing: Easier to test isolated logic
  • Composition: Build complex hooks from simple ones
  • Convention: Must start with "use"
Pattern: Custom hooks are JavaScript functions that use other hooks

useFetch Custom Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Failed');
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}
  • Encapsulates: Loading states, error handling
  • Returns object: Multiple values for flexibility
  • Reusable: Any component can fetch data
  • Dependencies: Re-fetches when URL changes

useLocalStorage Hook

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('LocalStorage error:', error);
    }
  };

  return [storedValue, setValue];
}
  • Persistent state: Survives page refreshes
  • API like useState: Familiar interface
  • Error handling: Graceful fallbacks
  • Use cases: User preferences, draft data

Composing Custom Hooks

// Combine multiple hooks
function useAuthenticatedFetch(url) {
  const { token } = useAuth();  // Another custom hook
  const { data, loading, error } = useFetch(
    token ? `${url}?token=${token}` : null
  );

  return {
    data,
    loading,
    error: error || (!token && 'Not authenticated')
  };
}

// Usage in component
function UserProfile() {
  const { data: user, loading } = useAuthenticatedFetch('/api/user');

  if (loading) return <Spinner />;
  return <Profile user={user} />;
}
  • Hook composition: Build complex from simple
  • Business logic: Authentication + data fetching
  • Clean components: Logic extracted to hooks
  • Testability: Mock individual hooks

Custom Hook Best Practices

Rules: Follow the Rules of Hooks + naming convention
  • Naming: Always prefix with "use"
  • Pure functions: No side effects outside React
  • Return consistently: Same shape always
  • Document well: Clear API and examples
  • Test thoroughly: Use @testing-library/react-hooks
  • Keep focused: Single responsibility

Summary: Production React Patterns

React Router v6

  • Modern routing for SPAs
  • Nested routes with Outlet
  • Dynamic params and navigation

Performance Hooks

  • useMemo for computations
  • useCallback for functions
  • Profile before optimizing

useRef Patterns

  • DOM element access
  • Value persistence
  • Cleanup management

Custom Hooks

  • Reusable logic extraction
  • Composition patterns
  • Clean component code
You're Ready For: Building production React applications with professional patterns!