With go_router, how do you pass data to a route through path parameters, query parameters and extra, and read each one?
answer
- three channels, only two live in the URL
- colon segment in the GoRoute path
- state.pathParameters, a map of strings
- state.uri.queryParameters since go_router 10
- extra travels beside the location
basics
~10 sDeclare 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 sIn 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 linesimport '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
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.
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.
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.
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.