next.js: [NEXT-1160] Clicking Links in intercepted routes does not unmount the interceptor route
Verify canary release
- I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #41~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 31 16:00:14 UTC 2
Binaries:
Node: 18.12.1
npm: 8.19.2
Yarn: 1.22.19
pnpm: N/A
Relevant packages:
next: 13.4.2-canary.5
eslint-config-next: N/A
react: 18.2.0
react-dom: 18.2.0
typescript: N/A
Which area(s) of Next.js are affected? (leave empty if unsure)
App directory (appDir: true), Routing (next/router, next/navigation, next/link)
Link to the code that reproduces this issue
To Reproduce
- Open the codesandbox in a new separate tab in order to be able to see the actual browser URL
- Click on a photo in order to intercept the route and open the modal with the photo details.
- Instead of clicking on the overlay click the “go back” link instead, located just below the photo
Describe the Bug
When you click the Link
and the URL changes to /
the interceptor route remains mounted and does not go away.
Expected Behavior
I would expect that since the route was changed (via Link
no less) the interceptor page should disappear once the route that it was intercepting was changed.
Screencast from 11-05-2023 04:23:04 ΜΜ.webm
Which browser are you using? (if relevant)
No response
How are you deploying your application? (if relevant)
No response
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 43
- Comments: 59 (5 by maintainers)
Should really mark it as experimental feature considering how unfinished intercepting/parallel routes are. I’ve wasted too much time trying to get this to work.
@feedthejim Why is this issue closed? This is still a problem.
Also, it is somehow related to what @mkarajohn is saying -
router.push()
doesn’t seem to work either. Onlyrouter.back()
does, but in my case, I don’t want to userouter.back()
. When someone gets sent the link with the modal opened then they click on the button to close it,router.back()
doesn’t make sense. It would be better to callrouter.push()
and navigate the user to the route behind the modal.Working setup I have is here: https://github.com/lmatteis/nextgram
Apparently you need the right page.js that returns
null
at the right level to tell the slot to not render anything.Has there been any update or progress on this? Ive just been implementing some parallel / intercepting routes for a model preview and discovered it not possible to navigate from the preview modal to the actual page. I think this is the same issue as this thread unless i’m missing something with how to navigate from within the modal 🤷
Has there been any word of progress on this? this seems annoyingly buggy, especially for something outright recommended in the docs 😦
I fixed this by making the modal itself a client component and checking if it should show based on the pathname
Definitely a bug though, I shouldn’t have to do this. Are intercepting routes ready for production?
Just thought of it now, I guess it’s worth noting that you could make the layout a client component and conditionally render the modal based on the pathname to fix it entirely. Hopefully that could help for now until it’s sorted out.
@Apollo-XIV I was able make intercepted routes unmounted by creating dummy page routes under the
@slot
folder. For example, if we havepage.tsx
,foo/page.tsx
andbar/[id]/page.tsx
, creating@modal/page.tsx
,@modal/foo/page.tsx
and@modal/bar/[id]/page.tsx
helps with unmounting. If we open@modal/(.)foobar/page.tsx
and then navigate away from the intercepted/foobar
route, Next.js renders another route in the@modal
slot.You can find an example in https://github.com/vercel/next.js/issues/53170. It’s a bug report related to multiple route groups, but if you have just one group with one layout, the trick with dummy routes might do the unmounting for you.
so,when can the official solve this problem? This problem has caused great trouble
Same issue. In my case I’m trying to redirect to another page using
router.push()
however, while it redirects to the correct page, the modal stays up. My solution to this for now is just checking the current pathname like other users suggested.I tried changing the
Link
withand this still does not work, while
this works.
So,
router.back()
(moving back in the history stack) androuter.push()
(adding to the history stack) behave differently somehow for this case? They both change the url to/
in the end. I am confused at how it’s determined that in one case the route changed while in the other it’s treated as if it did not.Okay, I have done some significant work here to make this work for me. Here is the context:
I will do everything in my power to stay away from client-side state management. I love using routes to manage all of my locational state (including modals). I noticed that whenever I was doing this,
redirect("/profile")
did not redirect me back! Instead, my modals stayed open.Using hints from above, I discovered that what is really important is the
layout
on which your modal is being rendered. That is, you should be rendering your slot in thelayout
that minimally contains your intercept routes.So, if I have a route structure like such:
I really only want to be slotting in the minimal containing folder. Here, that would be
group
. Let’s discuss why we want this.Why Only Grab the Minimum?
Let’s start by describing our file structure. Here, I lay out what we do not want.
The
layout.tsx
here looks something like…Now, if I am in the
edit
modal and I redirect back to/profile
, am I changing the layout properties? No. I might argue that I should be, but right now you are not. Instead, you are just saying “rerender the layout in the state you already have”, which still contains your slot.How to grab the minimum
I’ll keep the answer simple: we want to use route groups.
There are some nuances here. First, remember that we do not want the
profile/layout.tsx
to be rendering the@dialog
slot. Do not do that. Instead, use the(modal)/layout.tsx
to render your dialog slot. Second, do not use(modal)/layout.tsx
to render children either. Only use it to render your dialog slot.Summary
Like @IvanRomanovski said- this is likely not a bug, we’re just not using the system right. Now, that’s arguably a bad take from a DX perspective and I do think that this should be changed. But hopefully this sheds some light onto the complexities behind this.
This is exactly what I needed to do to workaround this without additional code. I was missing @modal/page.tsx but had a page.tsx file in all subsequently nested @modal routes. e.g) @modal/(.)add/page.tsx, @modal/(.)edit/page.tsx.
It is not documented in Next docs this is required which is perhaps the biggest culprit that could easily be updated to ease a lot of developer pain from this.
So in summary for my case, the @modal needed a page.tsx null return similar to default.tsx to wipe out the intercepted route with
router.push()
after saving in the modal without needingrouter.back()
or any other code workarounds.If you aren’t use .tsx but .ts or .js in this file structure you have to have them match all the way through for it to work too.
Looks like it is somehow broken by design, hm. what if there are two intercepting routes, say, one to
/sign-in
and another one to/sign-up
, which both lead to a modal dialog, and there is a link from signin modal to signup modal and in other direction as well, which is a common scenario))Just to clarify, I’m very grateful for your comment. Sorry if my quote seemed too assertive.
I agree. One of the biggest selling points of migrating to NextJS
app/
directory for me was the intercept/parallel routes feature. It’s a bit disheartening right now to have fully migrated to it and so many things are broken.If these aren’t fixed, I will ultimately just have to go back to the old way of doing things (parallel/intercept routes with useSearchParams -> regular React state/manually change URL).
Any update on this? This issue still makes the feature completely unusable for many use cases, or extremely convoluted at minimum with all the workarounds needed.
I have also found another issue that simply breaks the router history if stumbled upon. My previous repo that was linked in this thread a while back has been updated to latest 13.4.20-canary.31. Repo: https://github.com/vetledv/repro-intercept
To reproduce this issue:
Example: Navigate to three random pages. Refresh the third page. Go to second page, won’t render. Refresh. Go back to the first intercepted page. Also won’t render.
I see now that this must be similar to @extrabright 's issue. Maybe a feature to pop all intercepted routes would be beneficial? Edit: Specifying if you want to intercept or not could be nice maybe. This issue would be avoided if it wasn’t possible to intercept from an intercepted route.
Intercepted routes is perfect for so many things I want to do, so I would love to see these issues ironed out. I would probably have no chance, but if someone can point me to the right places I would love to at least try to help out here.
You are an absolute lifesaver, you’ve easily saved me hours of finding a workaround. Thank you sm 😃)
Here’s a tiny example that reproduces the issue:
https://codesandbox.io/p/sandbox/nifty-bessie-d9632z
Adding
@modal/[...catchAll]/page.tsx
as suggested in the docs here didn’t help.Don’t want to use router.back due to the reasons described by @tiersept
+1 The Modal slot mechanism is buggy. I have forced redirects or used router.back, while the url changes, the ModalInterceptor always get executed. Sometimes, the new route Im trying to go to gets properly executed and others it never does, as if it ended on the intercepted route and thats it.
Yeah as far as I can tell catch all routes just don’t work for clearing an intercepted route. See https://github.com/vercel/next.js/issues/48719 and https://github.com/vercel/next.js/issues/49531
IMHO the cleanest workaround is to create a layout inside the slot and conditionally render
children
based on the output of theusePathname
hook. If you have the following folder structure:You can create the following layout inside
@modal
:If you have multiple intercepting routes this seems to be less messy than rendering conditionally inside the parent layout (especially if the parent layout is the root layout, which can’t be made a client component).
The cleanest solution would obviously be using catch-all routes, but they’re still broken.
This is still a big issue. Working off the oficial nextgram example for reference, if you want to link away to a different part of the app from the intercepted, parallel route modal you’re just stuck with it wherever you go…
Catch-all pattern suggested by the docs seems to do nothing. Placing a null returning
page.tsx
like @lmatteis suggested works if you’re linking back to/
but if you’re linking somewhere else in the app like/dashboard
then the modal just stays open…I know this is bit out of discussion but we also tried implementing this in our e-commerce but now we are trying to evaluate if the use case was right
in our case we made something that product details view would show product image and pricing details and some few other thing+add to cart button in the intercepted route
and in the actual route::
we would like like to see all the above + things like product description suggested products reviews…etc…
we tried finding way to expand from intercepted route to actual route and we could not find… (maybe something like (intercept=false so default behavior is intercept=true for backward compatibility) to the Link as a prop i don’t know how easy that is of course to implement
That could maybe also be tackled together with this coz we even ended up adding router called
mini-product/[id]
to intercept then route.push could go toproduct/[id]
but as the issue suggest that was not possible and also beats the all logic of our intended use case for instanceI suppose the only reason it works is because it is no longer rendering the layout containing the modal. And yeah, the catchAll or any of the suggestions does not seem to work. Would like to see this reopened.
@vetledv thanks for this, very informative, I played with it a little. Like you said, the grouping is the only thing that seems to be “fixing” this behaviour, nice find. The
[...catchAll]
route doesn’t seem to do anything, can be deleted without effect.And, again, like you said, it does not seem like “intended behaviour” considering you cannot return to
/
using aLink
, since it’s in the same group.Still not figured it out so any help is appreciated. Meanwhile I managed to get the behaviour I needed using a context provider which tracks the amount
pathname
changes made inside the modal. When I want to close the modals, I loop the counter, smth like this, sincerouter.back()
is the only way to close this intercepted routes:So far it works pretty good with no glitches. However, I am hoping for official support on this issue.
Okay so, in case it helps anyone, the only way to handle this right now is to isolate the
@slot
and thelayout.tsx
which uses it in it’s own space (either nested or using a route group), so that thislayout.tsx
does not cover the pages where your modal is not required. The nextgram example isn’t good for this because it’s the root layout that uses the slot (modal) so all pages you make in there will be covered by it.Ran into the same issue and the
usePathname
technique mentioned by @vetledv worked for me. 🙏 🙏 🙏Would be nice to have a proper way though
@mkarajohn I see now that it only works in my specific use page (I was linking to an equivalent of /hello in this example). Here is a repo that I believe should cover all the behaviours. This does very much not seem like intended behaviours. https://github.com/vetledv/repro-intercept
+1 I’m having this issue as well. Like OP, I’ve also tried using the the catch all route and putting default.tsx in various places but nothing works.
The only way for me to close the modal seems to be to use router.back() since using a Link to the previous page changes the URL but leaves the modal opened.