React|React operation principles and state management

React Router and History API: How Do SPAs Switch Pages?

1
React Router and History API: How Do SPAs Switch Pages?

There is a common mistake Chris makes when building a website with React for the first time. To navigate between pages, he uses the standard HTML <a> tag.

// ❌ Don't do this
<a href="/about">Go to About Page</a>

The moment he clicks the link, the screen flashes white, and the entire page refreshes. The React state he was managing so carefully is completely reset and lost. He intended to build an SPA (Single Page Application), but he made it behave like a traditional MPA (Multi Page Application).

What we want is magic: "The URL changes, but the page does not refresh."

Today, we dig into the History API and the principles of React Router, the core browser technologies that make this magic possible.

1. The SPA Dilemma: Tricking the URL

As the name suggests, an SPA has only one HTML page (index.html). However, users want to navigate between various "pages" like /home, /about, and /profile.

Here, a contradiction arises:

  • Browser's Default Behavior: When the URL in the address bar changes, the browser requests a new HTML file from the server for that address and refreshes the screen.
  • SPA's Goal: We want to change the URL, but we want to block the server request and simply swap out the screen (components) using JavaScript.
  • To solve this problem, HTML5 introduced the History API.

    2. The Engine: History API (pushState)

    The browser's window object contains a history object that manages the session history. The core method that React Router relies on is pushState.

    // browser-console.ts
    
    // 1. Current address: /home
    // 2. Execute pushState (data, title, new URL)
    window.history.pushState({ page: 'about' }, '', '/about');
    
    // 3. Result: The address bar changes to /about.
    // ✨ IMPORTANT: The browser DOES NOT send a request to the server, nor does it refresh!

    This method is essentially commanding the browser: "Quietly change the text in the address bar, but don't load anything."

    However, this alone is not enough. The address bar has changed, but the actual screen (View) remains the same. We need a "watcher" to detect that the address has changed and swap the components accordingly.

    3. The Driver: React Router

    React Router is a library that wraps the History API and connects it to React's state. Its operating principle is simpler than you might think.

  • Click Detection: The user clicks a <Link> component.
  • Block Default Behavior: Internally, it creates an a tag but calls e.preventDefault() to stop the browser from refreshing.
  • URL Change: It calls history.pushState() to change only the address bar.
  • State Update: It detects that the URL has changed and updates the location state inside the Router.
  • Re-render: Since the state has changed, React triggers a re-render and displays only the component (Route) that matches the current URL.
  • The Principle in Code

    // MiniRouter.tsx (Conceptual Implementation)
    import { useState, useEffect } from 'react';
    
    function MiniRouter() {
      const [path, setPath] = useState(window.location.pathname);
    
      useEffect(() => {
        // Event listener to detect the Back button (popstate)
        const onPopState = () => setPath(window.location.pathname);
        window.addEventListener('popstate', onPopState);
        return () => window.removeEventListener('popstate', onPopState);
      }, []);
    
      const navigate = (url: string) => {
        window.history.pushState({}, '', url); // 1. Change URL
        setPath(url); // 2. Change State -> Trigger Re-render
      };
    
      return (
        <div>
          <button onClick={() => navigate('/home')}>Home</button>
          <button onClick={() => navigate('/about')}>About</button>
          
          {/* 3. Swap component based on current path */}
          {path === '/home' && <Home />}
          {path === '/about' && <About />}
        </div>
      );
    }

    4. Deployment Caution (The Refresh Problem)

    Chris deployed the app, which worked perfectly on the development server (localhost), to a real server (S3, Nginx, etc.). However, when he accessed the /about page directly or refreshed the browser, he got a 404 Not Found error.

    Why?

    Because the browser requested a file named /about from the server. However, the server only has one file: index.html. A folder or file named /about does not physically exist.

    Solution: Fallback Configuration

    You must configure the server to "return index.html unconditionally, no matter what path comes in."

    Once index.html loads, JavaScript executes, looks at the current URL (/about), and renders the appropriate page.

    # nginx.conf example
    location / {
      try_files $uri $uri/ /index.html;
    }

    Key Takeaways

  • SPA Core: Swaps content without refreshing the page during navigation.
  • history.pushState(): The key API that changes the browser address without sending a server request.
  • React Router: Detects URL changes and renders the matching component (State change).
  • Deployment Config: A Fallback configuration that sends all requests to index.html is mandatory.

  • Now we know the principles of page navigation. But where should we handle data fetching or DOM manipulation within a component? It is time to properly handle the most used, yet most misunderstood hook: useEffect.

    Continuing in: "The Lies of useEffect's Dependency Array: Solving Stale Closures and When to Use useLayoutEffect”

    🔗 References

  • MDN - History API
  • React Router - Main Concepts
  • MDN - PopStateEvent