skip to content

With go_router, how do you pass data to a route through path parameters, query parameters and extra, and read each one?

level: juniorimportance: should knowfreq 55%

answer

  1. three channels, only two live in the URL
  2. colon segment in the GoRoute path
  3. state.pathParameters, a map of strings
  4. state.uri.queryParameters since go_router 10
  5. extra travels beside the location

basics

~10 s

Declare a :accountId segment in the GoRoute path and read state.pathParameters['accountId']; append ?month=2026-09 and read state.uri.queryParameters['month']; pass an object with extra: and read state.extra, which never appears in the URL.

solid answer

~40 s

In go_router a `GoRoute(path: '/accounts/:accountId')` captures the segment, and the builder reads it from `state.pathParameters['accountId']` as a `String`. Optional values go in the query string and are read from `state.uri.queryParameters`; the old `state.queryParameters` getter was replaced by `state.uri` in go_router 10. Anything else can ride along as `extra` (`context.go('/accounts/42', extra: account)`) and is read from `state.extra`, but it is not part of the location: a deep link or a typed URL never carries it, and a non-JSON object without an `extraCodec` is dropped when go_router serialises the navigation state. So identity goes in the path, filters in the query, and `extra` only for optional, rebuildable hints.

code

dart · 36 lines
dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class AccountSummary {
  const AccountSummary(this.id, this.name);
  final String id;
  final String name;
}

class TransactionsScreen extends StatelessWidget {
  const TransactionsScreen({super.key, required this.accountId, this.month, this.summary});
  final String accountId;
  final String? month;
  final AccountSummary? summary;

  @override
  Widget build(BuildContext context) =>
      Text('${summary?.name ?? accountId} ${month ?? 'all months'}');
}

final GoRoute transactionsRoute = GoRoute(
  path: '/accounts/:accountId/transactions',
  builder: (BuildContext context, GoRouterState state) => TransactionsScreen(
    accountId: state.pathParameters['accountId']!,
    month: state.uri.queryParameters['month'],
    summary: state.extra as AccountSummary?,
  ),
);

void openTransactions(BuildContext context, AccountSummary summary) {
  final String location = Uri(
    path: '/accounts/${summary.id}/transactions',
    queryParameters: <String, String>{'month': '2026-09'},
  ).toString();
  context.go(location, extra: summary);
}

go deeper

for a junior

Recall the three channels: a colon segment read from state.pathParameters, the query string read from state.uri.queryParameters, and extra read from state.extra.

for a middle

Explain why path and query survive deep links while extra does not, and what go_router does with a non-JSON extra when no extraCodec is set.

for a senior

Show you design locations so every screen can be rebuilt from its URL alone, treating extra as an optional cache rather than a required input.

for a principal

Frame URL design as a contract: shareable, restorable locations cost some parsing, while extra-heavy navigation trades that robustness for convenience.

## Three ways to hand data to a go_router route go_router maps a **location** (a URL-shaped string such as `/accounts/42/transactions?month=2026-09`) to one or more `GoRoute` builders. Every builder receives a `GoRouterState`, and that object is where all incoming data is read. There are three channels, and they differ in one decisive property: whether the value survives outside the running app. | Channel | Declared as | Read with | Part of the URL? | Type | |---|---|---|---|---| | Path parameter | `:accountId` segment in `GoRoute.path` | `state.pathParameters['accountId']` | yes, required | `String` | | Query parameter | `?month=2026-09` appended to the location | `state.uri.queryParameters['month']` | yes, optional | `String` | | `extra` | `extra:` argument of `go` / `push` | `state.extra` | no | `Object?` | ## Path parameters A path segment prefixed with a colon becomes a named parameter. `GoRoute(path: '/accounts/:accountId')` matches `/accounts/42` and `/accounts/savings-7`, and the builder receives `{'accountId': '42'}` in `state.pathParameters`, a `Map<String, String>`. Points worth knowing: - Values are **always strings**, already URL-decoded by go_router; parsing to `int` or an enum is your job (or go_router_builder's, which generates typed fields). - A parameter can be constrained with a regular expression in parentheses, for example `:accountId(\d+)`; a segment that does not satisfy it lets matching continue with the next candidate route. - Matching is **case sensitive by default** since go_router 15.0: `GoRoute.caseSensitive` defaults to `true`, so `/Accounts/42` does not match `/accounts/:accountId` unless that route sets `caseSensitive: false`. - Nested routes accumulate parameters: a child `GoRoute(path: 'transactions/:txId')` under the account route sees both `accountId` and `txId`. ## Query parameters Anything after `?` is parsed into `state.uri`, a Dart `Uri`. Read single values from `state.uri.queryParameters` and repeated keys from `state.uri.queryParametersAll`. Query parameters are never declared in the route; every route accepts them, so the builder must tolerate their absence. Build locations with the standard `Uri` class rather than string concatenation so values are encoded correctly: `Uri(path: '/accounts/42/transactions', queryParameters: {'month': '2026-09'}).toString()`. Named navigation (`context.goNamed('transactions', pathParameters: {...}, queryParameters: {...})`) builds the same location from a route `name`. ## extra: an object beside the location `context.go(location, extra: value)` and `context.push(location, extra: value)` attach any Dart object, which the destination reads from `state.extra` (cast it, because it is `Object?`). It is convenient for handing over an already-loaded object, but it has sharp limits: 1. A location opened from outside the app, typed in a browser or shared as a link carries **no** `extra`. 2. go_router stores `extra` with the navigation state for browser history and state restoration. By default it JSON-encodes it; an object that is not JSON-encodable is replaced by `null`, with a logged warning suggesting a codec. 3. Passing `extraCodec` (a `Codec<Object?, Object?>`) to `GoRouter` lets you serialise complex objects properly. ## What GoRouterState holds for one location Take the location `/accounts/42/transactions?month=2026-09` matched by a route `transactions` nested under `/accounts/:accountId`. Inside the transactions builder, the state object reports: | Property | Value | |---|---| | `state.uri` | the full `Uri`, including `?month=2026-09` | | `state.matchedLocation` | `/accounts/42/transactions`, the matched part without the query | | `state.fullPath` | `/accounts/:accountId/transactions`, the template | | `state.pathParameters` | `{'accountId': '42'}` | | `state.uri.queryParameters` | `{'month': '2026-09'}` | | `state.extra` | whatever object was passed, or `null` | `matchedLocation` is the property redirects usually compare against, because it ignores the query string. `fullPath` is handy for logging which template matched without recording the ids themselves. Outside a builder, `GoRouterState.of(context)` returns the state for the route that encloses that context. ## Choosing the channel - Use the **path** for the identity of what the screen shows: the account id, the transaction id. - Use the **query** for optional view state that should survive a shared link: the month filter, a sort order. - Use **extra** only for data the screen can rebuild from the path if it is missing, such as an account summary already fetched on the previous screen to avoid a flash of loading. A screen that can render only when `extra` is present is a screen that breaks on deep links, web refresh after restoration, and any typed URL. ## Version notes go_router 7 renamed `params` and `queryParams` to `pathParameters` and `queryParameters`; go_router 10 then replaced `GoRouterState.location`, `queryParameters` and `queryParametersAll` with the single `uri` property. Tutorials that call `state.queryParameters` or `state.params` predate those releases and no longer compile against go_router 18.

  • With go_router, what type do path parameter values have, and who converts them?
    They are always `String` values in a `Map<String, String>`, URL-decoded by go_router. Converting `'42'` to an `int` or to an enum is up to the builder, or to go_router_builder, which generates typed constructor fields and the conversion code.
  • With go_router, why can state.extra be null on a screen that was opened with an extra object?
    The screen may have been reached by a deep link or a typed URL, which carry no `extra`, or its state was rebuilt from serialised history. Without an `extraCodec`, go_router JSON-encodes `extra` and replaces a non-encodable object with `null`, logging a warning.
  • With go_router 18, does /Accounts/42 match a GoRoute whose path is '/accounts/:accountId'?
    Not by default. Since go_router 15.0 matching is case sensitive and `GoRoute.caseSensitive` defaults to `true`. Setting `caseSensitive: false` on that route makes it match any casing.

saying these in an interview costs you the question

  • Reads query values from state.queryParameters, which go_router 10 replaced with state.uri.
  • Believes extra is encoded into the URL and survives a shared link.
  • Expects state.pathParameters to hold an int when the segment looks numeric.
  • Passes the whole account object through extra for a deep-linkable screen.
  • Assumes go_router route matching ignores letter case by default.