Routing med React Router: Navigation i SPA:er

I traditionella webbapplikationer hanteras navigation av servern - varje URL motsvarar en specifik fil eller endpoint. I Single Page Applications behöver vi client-side routing för att hantera navigation utan att ladda om hela sidan.

React Router är det de facto-standardbiblioteket för routing i React-applikationer.

Mål: Lära sig React Router, implementera BrowserRouter, konfigurera routes, hantera navigation programmatiskt och skapa protected routes.

Installation och Grundkonfiguration

Installation

npm install react-router-dom

Grundläggande Router Setup

// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Router>
      <Layout>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </Layout>
    </Router>
  );
}

export default App;

Router-typer

React Router erbjuder flera router-typer för olika användningsfall:

graph TD
    A[React Router] --> B[BrowserRouter]
    A --> C[HashRouter]
    A --> D[MemoryRouter]
    
    B --> E["Clean URLs<br/>/about, /products"]
    C --> F["Hash URLs<br/>/#/about, /#/products"]
    D --> G["Memory only<br/>(testing, React Native)"]
    
    style B fill:#98fb98
    style E fill:#98fb98

BrowserRouter (Rekommenderad)

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

// Ger rena URLs: example.com/about
function App() {
  return (
    <BrowserRouter>
      {/* Routes här */}
    </BrowserRouter>
  );
}

HashRouter (Fallback)

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

// Ger hash URLs: example.com/#/about
// Användbart för statiska hostar som GitHub Pages
function App() {
  return (
    <HashRouter>
      {/* Routes här */}
    </HashRouter>
  );
}

Routes och Route Configuration

Grundläggande Routes

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

function AppRoutes() {
  return (
    <Routes>
      {/* Exact match för hem-sidan */}
      <Route path="/" element={<Home />} />
      
      {/* Enkla routes */}
      <Route path="/about" element={<About />} />
      <Route path="/services" element={<Services />} />
      <Route path="/contact" element={<Contact />} />
      
      {/* 404 - måste vara sist */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Nested Routes (Nästlade routes)

// Huvudroutes
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Layout />}>
          {/* Child routes */}
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="products/*" element={<ProductRoutes />} />
        </Route>
      </Routes>
    </Router>
  );
}

// Layout-komponent med Outlet
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header>
        <Navigation />
      </header>
      <main>
        <Outlet /> {/* Här renderas child routes */}
      </main>
      <footer>
        <Footer />
      </footer>
    </div>
  );
}

// Produktroutes som separata komponenter (v6, relativa paths)
function ProductRoutes() {
  return (
    <Routes>
      <Route index element={<ProductList />} />
      <Route path=":id" element={<ProductDetail />} />
      <Route path="categories" element={<ProductCategories />} />
      <Route path="categories/:category" element={<CategoryProducts />} />
    </Routes>
  );
}

Dynamiska Routes med Parameters

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

// Route configuration
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId/comments/:commentId" element={<Comment />} />

// I komponenten
function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <div>Laddar...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Query parameters
function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category');
  const sort = searchParams.get('sort');

  const updateSearch = (newCategory) => {
    setSearchParams({ category: newCategory, sort });
  };

  return (
    <div>
      <h2>Produkter {category && `i kategorin ${category}`}</h2>
      <button onClick={() => updateSearch('electronics')}>
        Elektronik
      </button>
    </div>
  );
}
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      {/* Grundläggande länkar */}
      <Link to="/">Hem</Link>
      <Link to="/about">Om oss</Link>
      <Link to="/products">Produkter</Link>
      
      {/* Dynamiska länkar */}
      <Link to={`/users/${user.id}`}>Min profil</Link>
      
      {/* Med state (för att skicka data) */}
      <Link 
        to="/checkout" 
        state={{ from: 'cart', items: cartItems }}
      >
        Till kassan
      </Link>
      
      {/* Ersätt current history entry */}
      <Link to="/login" replace>
        Logga in
      </Link>
    </nav>
  );
}
import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink 
        to="/" 
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        Hem
      </NavLink>
      
      <NavLink 
        to="/products" 
        style={({ isActive }) => ({
          color: isActive ? 'red' : 'blue'
        })}
      >
        Produkter
      </NavLink>
      
      {/* Med custom active class (v6) */}
      <NavLink 
        to="/about"
        className={({ isActive }) => `nav-link ${isActive ? 'current' : ''}`}
      >
        Om oss
      </NavLink>
    </nav>
  );
}

Programmatisk Navigation (varför)

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

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

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      
      // Navigera tillbaka till föregående sida eller hem
      const from = location.state?.from?.pathname || '/';
      navigate(from, { replace: true });
    } catch (error) {
      setError('Inloggning misslyckades');
    }
  };

  const goBack = () => {
    navigate(-1); // Gå tillbaka i historiken
  };

  const goToProducts = () => {
    navigate('/products', { 
      state: { from: 'login' },
      replace: false 
    });
  };

  return (
    <form onSubmit={handleLogin}>
      {/* Formulärfält */}
      <button type="button" onClick={goBack}>
        Tillbaka
      </button>
      <button type="submit">
        Logga in
      </button>
    </form>
  );
}

Vidare läsning

Fler routing‑ämnen när du är redo: skyddade routes (auth), data‑laddare, central felhantering, och animerade övergångar. Dessa bör komma efter att grunderna sitter.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'products',
        element: <ProductsLayout />,
        children: [
          {
            index: true,
            element: <ProductList />
          },
          {
            path: ':id',
            element: <ProductDetail />,
            loader: async ({ params }) => {
              return fetch(`/api/products/${params.id}`);
            }
          }
        ]
      },
      {
        path: 'admin',
        element: <ProtectedRoute requiredRole="admin"><AdminLayout /></ProtectedRoute>,
        children: [
          {
            path: 'users',
            element: <UserManagement />
          },
          {
            path: 'settings',
            element: <Settings />
          }
        ]
      }
    ]
  }
]);

function App() {
  return <RouterProvider router={router} />;
}
// Modern data loading pattern (React Router v6.4+)
const router = createBrowserRouter([
  {
    path: '/products/:id',
    element: <ProductDetail />,
    loader: async ({ params }) => {
      const product = await fetch(`/api/products/${params.id}`);
      if (!product.ok) {
        throw new Response('Product not found', { status: 404 });
      }
      return product.json();
    },
    errorElement: <ProductError />
  }
]);

// I komponenten
function ProductDetail() {
  const product = useLoaderData();
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <Routes location={location} key={location.pathname}>
        <Route 
          path="/" 
          element={
            <motion.div
              initial={{ opacity: 0, x: -100 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: 100 }}
              transition={{ duration: 0.3 }}
            >
              <Home />
            </motion.div>
          } 
        />
        {/* Andra routes... */}
      </Routes>
    </AnimatePresence>
  );
}

Error Boundaries för Routes

class RouteErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Route error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-page">
          <h2>Något gick fel</h2>
          <p>Vi ber om ursäkt för besväret.</p>
          <Link to="/">Gå tillbaka till startsidan</Link>
        </div>
      );
    }

    return this.props.children;
  }
}

// 404 Component
function NotFound() {
  const location = useLocation();

  return (
    <div className="not-found">
      <h1>404 - Sidan hittades inte</h1>
      <p>Sidan <code>{location.pathname}</code> existerar inte.</p>
      <Link to="/">Gå till startsidan</Link>
    </div>
  );
}

Best Practices

1. Route Organization

// routes/index.js - Centraliserad route configuration
export const routes = {
  home: '/',
  about: '/about',
  products: '/products',
  productDetail: (id) => `/products/${id}`,
  userProfile: (userId) => `/users/${userId}`,
  admin: {
    base: '/admin',
    users: '/admin/users',
    settings: '/admin/settings'
  }
};

// Användning
<Link to={routes.productDetail(product.id)}>
  {product.name}
</Link>

2. Route Guards

// Reusable route guard component
function RouteGuard({ 
  children, 
  requireAuth = false, 
  requiredRole = null,
  fallback = null 
}) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <LoadingSpinner />;

  if (requireAuth && !user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredRole && (!user || user.role !== requiredRole)) {
    return fallback || <Navigate to="/unauthorized" replace />;
  }

  return children;
}

3. SEO-vänliga Routes

// Dynamiska titles baserat på route
function useDocumentTitle(title) {
  useEffect(() => {
    const prevTitle = document.title;
    document.title = title;
    return () => {
      document.title = prevTitle;
    };
  }, [title]);
}

function ProductDetail() {
  const { id } = useParams();
  const [product, setProduct] = useState(null);

  useDocumentTitle(product ? `${product.name} - Min Butik` : 'Laddar...');

  // ...
}

Sammanfattning

React Router är kraftfullt verktyg för navigation i React-applikationer:

  • BrowserRouter ger rena URLs och är bästa valet för de flesta applikationer
  • Routes och Route konfigurerar URL-mappningar till komponenter
  • Link och NavLink skapar navigerbara länkar
  • useNavigate möjliggör programmatisk navigation
  • Protected Routes skyddar känsligt innehåll
  • Dynamic routes hanterar parametrar och query strings

Nästa steg är att lära sig konsumera API:er för att hämta och skicka data till backend-tjänster.