Skip to main content
Add client-side routing to your Jaspr application using the jaspr_router package for seamless navigation without full page reloads.

Installation

Add jaspr_router to your project:
pubspec.yaml
dependencies:
  jaspr: ^0.22.0
  jaspr_router: ^0.8.0
Then run:
dart pub get

Basic setup

Wrap your application with a Router component and define routes:
import 'package:jaspr/jaspr.dart';
import 'package:jaspr_router/jaspr_router.dart';

class App extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return Router(
      routes: [
        Route(
          path: '/',
          builder: (context, state) => HomePage(),
        ),
        Route(
          path: '/about',
          builder: (context, state) => AboutPage(),
        ),
        Route(
          path: '/contact',
          builder: (context, state) => ContactPage(),
        ),
      ],
    );
  }
}

Route parameters

Capture dynamic segments in your routes:
Route(
  path: '/users/:id',
  builder: (context, state) {
    final userId = state.pathParameters['id']!;
    return UserProfile(userId: userId);
  },
),
Access parameters in your component:
class UserProfile extends StatelessComponent {
  final String userId;
  
  const UserProfile({required this.userId});
  
  @override
  Component build(BuildContext context) {
    return div([
      h1([text('User Profile: $userId')]),
      // Fetch and display user data
    ]);
  }
}

Query parameters

Access URL query parameters from the route state:
Route(
  path: '/search',
  builder: (context, state) {
    final query = state.uri.queryParameters['q'] ?? '';
    final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
    
    return SearchResults(query: query, page: page);
  },
),
Use the Link component for client-side navigation:
import 'package:jaspr_router/jaspr_router.dart';

class Navigation extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return nav([
      Link(
        to: '/',
        child: text('Home'),
      ),
      Link(
        to: '/about',
        child: text('About'),
      ),
      Link(
        to: '/contact',
        child: text('Contact'),
      ),
    ]);
  }
}
Highlight the currently active route:
class NavLink extends StatelessComponent {
  final String to;
  final String label;
  
  const NavLink({required this.to, required this.label});
  
  @override
  Component build(BuildContext context) {
    final currentPath = Router.of(context).location;
    final isActive = currentPath == to;
    
    return Link(
      to: to,
      child: text(label),
      styles: Styles(
        color: isActive ? Color.hex('#01589B') : Colors.black,
        fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
        textDecoration: TextDecoration.none,
      ),
    );
  }
}

Programmatic navigation

Navigate programmatically using Router.of(context):
class LoginButton extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return button(
      onClick: (event) {
        // Perform login logic
        final success = login();
        
        if (success) {
          // Navigate to dashboard
          Router.of(context).push('/dashboard');
        }
      },
      [text('Login')],
    );
  }
}
final router = Router.of(context);

// Push a new route
router.push('/about');

// Replace current route (no back button)
router.replace('/login');

// Go back
router.back();

// Push with named route
router.pushNamed('user', pathParameters: {'id': '123'});

Nested routes

Create hierarchical route structures:
Route(
  path: '/dashboard',
  builder: (context, state) => DashboardLayout(),
  routes: [
    Route(
      path: 'overview',
      builder: (context, state) => DashboardOverview(),
    ),
    Route(
      path: 'settings',
      builder: (context, state) => DashboardSettings(),
    ),
    Route(
      path: 'users/:id',
      builder: (context, state) => DashboardUserDetail(
        userId: state.pathParameters['id']!,
      ),
    ),
  ],
),
Layout component with nested outlet:
class DashboardLayout extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return div([
      // Sidebar navigation
      aside([
        Link(to: '/dashboard/overview', child: text('Overview')),
        Link(to: '/dashboard/settings', child: text('Settings')),
        Link(to: '/dashboard/users', child: text('Users')),
      ]),
      // Nested route content
      main_([
        // Child routes render here
      ]),
    ]);
  }
}

Named routes

Define named routes for easier navigation:
Router(
  routes: [
    Route(
      name: 'home',
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    Route(
      name: 'user',
      path: '/users/:id',
      builder: (context, state) => UserProfile(
        userId: state.pathParameters['id']!,
      ),
    ),
  ],
)
Navigate using route names:
Router.of(context).pushNamed(
  'user',
  pathParameters: {'id': '123'},
  queryParameters: {'tab': 'profile'},
);

Redirects

Redirect users to different routes:
Route(
  path: '/old-path',
  redirect: (context, state) => '/new-path',
),

// Conditional redirect
Route(
  path: '/admin',
  redirect: (context, state) {
    final isAuthenticated = checkAuth();
    return isAuthenticated ? null : '/login';
  },
  builder: (context, state) => AdminPanel(),
),

404 Not Found page

Handle unknown routes with a catch-all:
Router(
  routes: [
    // ... your routes
  ],
  errorBuilder: (context, state) => NotFoundPage(),
)
Example 404 page:
class NotFoundPage extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return div([
      h1([text('404 - Page Not Found')]),
      p([text('The page you are looking for does not exist.')]),
      Link(
        to: '/',
        child: text('Go to Home'),
      ),
    ]);
  }
}

Route guards

Protect routes with authentication checks:
Route(
  path: '/profile',
  builder: (context, state) {
    final user = getCurrentUser();
    
    if (user == null) {
      // Redirect to login
      Future.microtask(() {
        Router.of(context).push('/login');
      });
      return LoadingPage();
    }
    
    return ProfilePage(user: user);
  },
),

Lazy loading routes

Load route components on demand to reduce initial bundle size:
import 'heavy_component.dart' deferred as heavy;

Route(
  path: '/heavy',
  builder: (context, state) {
    return LazyRoute(
      loader: () async {
        await heavy.loadLibrary();
        return heavy.HeavyComponent();
      },
    );
  },
),
Lazy route component:
class LazyRoute extends StatefulComponent {
  final Future<Component> Function() loader;
  
  const LazyRoute({required this.loader});
  
  @override
  State<LazyRoute> createState() => _LazyRouteState();
}

class _LazyRouteState extends State<LazyRoute> {
  Component? loadedComponent;
  
  @override
  void initState() {
    super.initState();
    component.loader().then((comp) {
      setState(() {
        loadedComponent = comp;
      });
    });
  }
  
  @override
  Component build(BuildContext context) {
    return loadedComponent ?? div([text('Loading...')]);
  }
}

Complete navigation example

Here’s a complete multi-page app with navigation:
import 'package:jaspr/jaspr.dart';
import 'package:jaspr_router/jaspr_router.dart';

class App extends StatelessComponent {
  @override
  Component build(BuildContext context) {
    return Router(
      routes: [
        Route(
          name: 'home',
          path: '/',
          builder: (context, state) => Layout(
            child: HomePage(),
          ),
        ),
        Route(
          name: 'products',
          path: '/products',
          builder: (context, state) => Layout(
            child: ProductsPage(),
          ),
        ),
        Route(
          name: 'product',
          path: '/products/:id',
          builder: (context, state) => Layout(
            child: ProductDetail(
              productId: state.pathParameters['id']!,
            ),
          ),
        ),
        Route(
          name: 'cart',
          path: '/cart',
          builder: (context, state) => Layout(
            child: CartPage(),
          ),
        ),
      ],
      errorBuilder: (context, state) => Layout(
        child: NotFoundPage(),
      ),
    );
  }
}

class Layout extends StatelessComponent {
  final Component child;
  
  const Layout({required this.child});
  
  @override
  Component build(BuildContext context) {
    return div([
      header([
        nav([
          NavLink(to: '/', label: 'Home'),
          NavLink(to: '/products', label: 'Products'),
          NavLink(to: '/cart', label: 'Cart'),
        ]),
      ]),
      main_([
        child,
      ]),
      footer([
        text('© 2024 My Store'),
      ]),
    ]);
  }
}

Best practices

Named routes make refactoring easier and reduce typos:
// Good
router.pushNamed('user', pathParameters: {'id': userId});

// Avoid
router.push('/users/$userId');
Complex logic should be in components, not route builders:
// Good
Route(
  path: '/dashboard',
  builder: (context, state) => DashboardPage(),
)

// Avoid
Route(
  path: '/dashboard',
  builder: (context, state) {
    // Complex logic here
    final user = fetchUser();
    final settings = loadSettings();
    // ...
  },
)
Show loading indicators while lazy loading routes or fetching data

Next steps

Router API

Complete Router component reference

Link API

Link component documentation

jaspr_router plugin

Advanced routing features

State management

Manage state across routes

Build docs developers (and LLMs) love