-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[go_router] Support for top level onEnter
callback.
#8339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Added onEnter callback to enable route interception and demonstrate usage in example.
Hi @chunhtai, This PR introduces a top-level onEnter callback, addressing critical gap in go_router’s handling of deep links and redirect customizations. It allows developers to intercept routes, block navigation, or implement custom logic. Given its potential impact, your feedback would be greatly appreciated. Thank you for your time! |
…nd enhanced OnEnter test.
Hi @chunhtai, Following up on this PR. I've updated the implementation to:
typedef OnEnter = bool Function(
BuildContext context,
GoRouterState currentState,
GoRouterState nextState
);
Let me know if you'd like any adjustments to this approach. |
Provides access to GoRouter within OnEnter callback to support navigation during early routing stages when InheritedGoRouter is not yet available in the widget tree.
Hi @chunhtai, I've expanded the The implementation:
|
The callback signature makes sense to me. The fallback router makes less sense to me. Since it's not the right instance, why would one use it ? Could you add its usage in the example ? |
Hi @cedvdb, The callback signature is indeed straightforward. The fallback router isn’t ideal in that it isn’t coming directly from the widget tree (via GoRouter.of(context)), but it serves as a backup when the Inherited widget isn’t available yet—typically on app startup. In those cases, if you try to look up the router from context, you’d get null because the tree isn’t fully built. Instead, the fallback (usually provided as this when constructing the parser) lets your onEnter callback still have a router reference so you can, for example, call router.go(...) to redirect or modify navigation. Here’s an example usage: GoRouter(
onEnter: (context, currentState, nextState, router) {
// Even if GoRouter.of(context) is null (because the tree isn’t built yet),
// the fallback router will be non-null.
if (nextState.uri.path == '/referral') {
// Use the router instance (either from context or fallback) to navigate
router.go('/handle-referral');
return false; // Prevent default navigation.
}
return true;
},
// other config...
) So while it isn’t “the right instance” from context when the tree isn’t ready, it’s our best–available router reference. If someone’s using the router during startup (or in any context where the inherited widget isn’t available), the fallback router provides a safe way to still programmatically navigate. The idea is that the fallback isn’t a “dummy” or incomplete instance—it’s the same router instance (typically provided as If you think this fallback might lead to unexpected behavior, one option is to document it clearly and advise that it’s only intended as a temporary solution until the tree is built. Otherwise, it’s our practical solution for early navigation calls. |
My bad, I misread the code and thought the fallback router was a parameter of the Router constructor, that we had to define ourselves, but that was the RouterInformationParser constructor I was reading. I will try this out on a decently sized project |
@Deprecated( | ||
'Use onEnter instead. ' | ||
'This feature will be removed in a future release.', | ||
) | ||
final int redirectLimit; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to handle infinite redirect, I thought we may still want to keep this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chunhtai Is there any reason to have this public ? An arbitrary limit could be picked, eg: max 5 times on the same route and kept internal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we let people pick their own limit, it was here since the v4 before flutter team took ownership. I find no reason to deprecate it, so it has been hanging around since.
); | ||
|
||
/// The signature of the onEnter callback. | ||
typedef OnEnter = bool Function( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the conclusion is that we should return a resolution class where the redirect login will a callback in the returned object.
This will help us to only run the redirection after we get the resolution. I think this will reduce a lot of obscure corner case where the another round of redirection starts before the previous round has finished
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, stateful behaviors in navigation leads to unpredictable behavior on the web, that is: on the web an user can simply refresh a page and the state will be lost. Stateless redirection and re-redirection is achievable through query parameters (and is standard on the web)
I guess you could make the case that not every one has the web as a target and a stateful redirect is convenient. Still, being restrictive here by requiring statelessness should be considered imo
Maybe an enum may be more understandable than a boolean, else you gotta remember if true means "handled" or "you should handle it" and refer to the doc every time. "stop" and "next" are clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cedvdb yes while a class-based result might seem more expressive initially, it introduces statefulness that can be problematic on the web. We should lean toward a design that is inherently stateless, leveraging the URL (and query parameters) to persist any transient state needed during redirection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, if it has to return a stateless object, then using enum will be more preferable then boolean. I vaguely remembered I made this decision because there may be resolution other than a yes and no if we were to expand this API.
As return the class, or more specifically doing the redirection in the callback is that i want to avoid corner cases like this
Resolution onEnter(...) {
router.go('other route');
await somethingasync();
return false;
}
when this onEnter is called, the router.go will trigger the onEnter again before the entire redirection pipeline is finished, it is basically a non-tail recursion.
If we do this
Resolution onEnter(...) {
return Resolution.stop(callBack: () {
router.go('other route');
await somethingasync();
});
}
the route parser can finishes the redirecting pipeline before calling the callback, which will end up being a tail recursion.
My assumption is the non-tail recursion can create obscure bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that returning a plain boolean limits us to a yes/no decision, and as you've noted, if we let the side effect (like calling router.go('other route')) happen directly in the callback, it can trigger a reentrant call before the entire redirection pipeline is finished. That non-tail recursion is a recipe for obscure bugs.
By having the callback return a Resolution—an enum that can represent more than just “yes” or “no” - we can include a callback (or similar mechanism) that defers the actual navigation change until after the redirect pipeline is fully processed. In other words, by returning something like:
Resolution.stop(callback: () async {
await somethingAsync();
router.go('other route');
});
we ensure that the router.go call isn’t executed until after the parser has finished its work. This effectively makes the redirection pipeline tail-recursive, avoiding the pitfall of re-triggering onEnter mid-flight.
Also I think using an enum here is more future-proof, too .... it gives us room to add more nuanced resolutions if needed. This stateless design fits well with web navigation paradigms where state isn’t preserved across refreshes, and it avoids the pitfalls of having stateful redirection logic.
Imo onEnter should be made asynchronous instead. One may query a local cache inside for example. I sent a PR on your fork that doesn't break the synchronous future usage |
@cedvdb thanks for the review, I've reviewed the changes in my last commit, and I believe I've resolved the issues effectively:
@chunhtai Regarding the discussion about using an enum (or even a class-based resolution) for a more expressive API, Look at the changes guys and let me know ur thoughts. |
- Change onEnter callback signature to FutureOr<bool> for async operations. - Update _processOnEnter for asynchronous navigation decisions. - Use last successful match as fallback on blocked navigation, preventing recursion. - Remove deprecated fallbackRouter parameter. - Adjust redirect limit and loop detection for stable fallback endpoints. - Update tests for sync, async, and loop scenarios. - Improve documentation of design decisions.
@cedvdb, thanks for the comment and the PR! I saw the issue you encountered in your PR. I've pushed some updates for the Here's what I did:
Take another look at the latest changes, especially the tests in |
Isn't that unintuitive for the end users ? Can you explain what is the issue when await is used ? I suppose it's complex, ence why you had to modify the test |
I think I miss explained it, my previous explanation about the state conflict being inherent to I meant to explain why I thought there was a problem with the test case u mentioned. it turns out the original issue was not with using developers can confidently use I have updated the test case again. It is actually using await with pushNamed but I made its logic closer to the one u created, juts a little bit enhanced. another thing I noticed in the failing test case in ur PR I added a comment there explaining why I changed this in the updated test case as well. |
I'm confused, could you clarify if you did fix the issue where |
Enhanced the test, and fixed the issue where currentState was determined incorrectly. |
@cedvdb any updates? |
Edit: nvm |
@cedvdb would you be able to share what are the complex scenarios you are using it? |
@omar-hanafy you may want to request reviews from the flutter team |
I've thoroughly investigated your concern about How // When navigating from /home to /about:
onEnter: (context, current, next, goRouter) async {
// current.uri = "/home" <- where we are now (before navigation)
// next.uri = "/about" <- where we're trying to go
// goRouter.state.uri = "/home" <- also the current location
return true; // Allow navigation
} And we actually have several test cases that verify this exact behavior. The most relevant one is the 'Should handle The Testing:
I also commented in you prev PR about that case and clarified whats done in the last push here. Could you help me understand your specific issue?
This will help me determine if there's an edge case I haven't considered or if it's a documentation issue that needs clarification. @chunhtai whats ur review on this btw ? |
I reran the test suite and it is now passing. I'm not 100% sure because I cannot run the whole test suite, but as far as I remember, the problematic test is now fixed. It doesn't seem like you made changes lately that should affect that, so it may be that the previous version from the cache before you fixed the issue was used on the last run. In other words, for me, it seems to work as intended now |
@cedvdb perfect! |
@omar-hanafy you probably want to reach on discord to ask for a review from the flutter team |
@chunhtai can you take a look? |
@omar-hanafy it looks like there are some merge conflicts. |
@Piinks It was just from a latest version update (In the changelog), probably from another PR, the conflicted files are resolved now. |
Description:
This PR introduces top level
onEnter
callback inRouteConfiguration
to allow developers to intercept and conditionally block navigation based on incoming routes. It adds an example (top_level_on_enter.dart
) demonstrating its usage, such as handling referral code from/referral
.What This PR Changes:
onEnter
callback (typedef OnEnter
) to intercept route navigation before processing.onEnter
inGoRouteInformationParser
.RouteConfiguration
to include theonEnter
callback.top_level_on_enter.dart
, to demonstrate the feature.onEnter
callback functionality.Simple usage example:
Impact:
Enables developers to implement route-specific logic, such as support for handling action-based deep links without navigation (160602)
Pre-launch Checklist:
dart format
.)[go_router]
.pubspec.yaml
with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes].CHANGELOG.md
to add a description of the change, [following repository CHANGELOG style], or this PR is [exempt from CHANGELOG changes].///
).